Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Publishing to Single-file in .Net Core #52

Merged
merged 12 commits into from
Feb 21, 2019

Conversation

swaroop-sridhar
Copy link
Contributor

The is a design proposal for supporting single file distribution in .Net Core.

To run apps published as a single-file, CoreCLR#20287 proposed extracting out all the embedded dependencies to files at startup.

However, based on customer feedback through public and private fourms, the current design uses a hybrid of in-memory execution and extraction to files.

Further stages of improvements are outlined in staging.md.

The documents in this checkin contain a proposal for supporting
single file distribution in .Net Core.

To support apps published as a single-file, [CoreCLR#20287](https://github.com/dotnet/coreclr/issues/20287)
proposed extracting out all the embedded dependencies to files at startup.
However, based on customer feedback through public and private fourms, the design
proposed here uses a hybrid of in-memory execution and extraction to files.

Further stages of improvements are outlined in staging.md.
@ericsampson
Copy link

For self-contained, what happens the second (and subsequent) runs of the .exe? I'd hope that it would be smart enough to only extract the files the first time.

@swaroop-sridhar
Copy link
Contributor Author

@ericsampson the proposal to reuse extracted files on subsequent runs is discussed here: https://github.com/dotnet/designs/pull/52/files#diff-5e93215a94e626495c10448a40fa4ccbR40

@ericsampson
Copy link

Thanks @swaroop-sridhar I did end up finding that, it just wasn't mentioned in the design.md user User Experience which makes it seem like the extraction was always going to happen each time: Cheers!

Run: HelloWorld.exe

The bundled app and configuration files are processed directly from the bundle.
Remaining 216 files will be extracted to disk at startup.

@swaroop-sridhar
Copy link
Contributor Author

Thanks for pointing that out @ericsampson. I added a note about it here

Copy link

@ayende ayende left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One thing that I haven't seen addressed is the issue of a platform dependent native libraries.
I assume that this bundled apps are per platform anyway, but how do I select which native deps I'll bundle with each?


* The `PublishSingleFile` property applies to both framework dependent and self-contained publish operations.
* Setting the `PublishSingleFile`property causes the managed app, dependencies, configurations, etc. (basically the contents of the publish directory when `dotnet publish` is run without setting the property) to be embedded within the native `apphost`. The publish directory will only contain the single bundled executable (and the symbol file).

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about embedded symbols in the file? I assume that would still work as usual, but the text make is seems like that would be separate?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I was only talking about the fact that PDB file will be separate and not embedded within the single-file app. I'll change the wording.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is actually a concern for me. I want to be able to have a single package that include the PDB. We currently do that with embedded pdbs, and I assume this will continue to work?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the feedback, we could make this configurable. I think by default (to save size on disk) this would be off.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ayende you're asking about assemblies generated with debug info embedded (ex: csc -debug:embedded) right? Those are fine, I'll add it as a scenario to test.
If a separate PDB file is generated, we won't add it to the single-file. But as @jeffschwMSFT says, we can make it configurable if necessary.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 on configurably embedded PDB.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I am. If those work, I'm happy with that.

- The bundler tool will mostly work as-is, regardless of whether we publish an application or class-lib. The binary blob with dependencies can be appended to both native and managed binaries.
- For host/runtime support, the options are:
- Implement plugins using existing infrastructure. For example: Take control of assembly/native binary loads via existing `AssemblyLoadContext` callbacks and events. Extract the files embedded within the single-file plugin using the `GetFileStream()` API and load them on demand.
- Have new API to load a single-file plugin, for example: `AssemblyLoadContext.LoadWithEmbeddedDependencies()`.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll also require a flag like IsBundled, to know how to behave, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thought was that LoadWithEmbeddedDependencies will only be used with bundled assemblies.
I agree that more thought is required about the necessity/details of the LoadWithEmbeddedDependencies API, while implementing support for single-file plugins.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ayende this is a good suggestion. Can you describe a potential scenario?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider the case of resources that are bundled into the app. In our case, we have a web app that has a lot of files (js, png, css, etc).
We actually have three separate options here right now. During dev, we might be working on the raw files, the compiled files or during release, loading them from a zip file.

Being able to tell how we are running would be helpful because if IsBundled == false, I need to probe a whole different location to see where the file I need to serve it located.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good feedback, and thanks for the scenario details.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, thanks for the details @ayende. For this situation, I think it would be better served by providing an abstraction that handles bundles and files seamlessly, as proposed here.

But I think it is also reasonable to expose an API like the following, to support custom scenarios.

namespace System.Runtime.Loader
{
    public partial class Bundle
    {
        public static bool IsBundle(Assembly assembly);
    }
}


* Extract to a location within the temp-directory, so that if some files are not removed (ex: because an app crashed before cleanup), they are removed when the temp-directory is purged.
* Be short, to reduce the risk of running over path length limit.
* Be randomized, in order to support concurrent launches of the same app.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This implies that each launch will extract the files from scratch? This can be a pretty big cost, no?
Can we do this only once?
What about if we'll use the file hashes and use that? This helps both concurrent instances and 2nd run.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reuse of extracted files is discussed in the next section; this section is about the option of extracting to temporary locations on every use.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We have discussed having reuse as an option. Both 'extract everytime' and 'reuse' have different pros and cons. We plan on leaving it up to the developer to choose.


* Simpler implementation: fewer complexities with respect to app-updates and concurrency.
* User need not worry about explicit cleanup.
* Some customers don't prefer a click-and-run model, rather than an app-install model. This option better suits them.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this statement

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Click-and-run model: App runs with necessary extraction on startup and cleanup on exit. There are no side-effects after the app is run.
Install model: App installs the extracted files to user-visible non-temporary storage, for reuse across multiple runs.

The point here is that the temporary-extraction solution suits situations that don't prefer apps leaving behind state on disk.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue is that Windows has to load the native library from a file, right?
In the click and run scenario, can you write the files with FILE_FLAG_DELETE_ON_CLOSE, call LoadLibrary on the file and then close it?
That would mean that the OS will take care of cleanup automatically.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @ayende. We can consider that for implementation on Windows.
However, we will still need a clean up step on exit, because there may be files other files (ex: data files) that are bundled and extracted out to disk.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can probably do that as well. Extract them, open with delete on close and "leak" the handle.
The OS will handle that for you.
On Linux, I think we can load a memfd_create and then dlopen , so we don't even need to touch the disk

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The sentence construction is confusing: "don't prefer...rather than". I do not know what you are saying.

Can you restate this intent in different words.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I'll fix the typo.


In this stage

* .Net Core native libraries are statically linked to the single-file host executable.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would love this option, because then the only files extracted would be the apps native dependencies, right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to be a great state. The approach of this proposal allows a step-wise approach so we don't have to have everything ready before we provide anything.


### 5.1 Description

This stage improves on the previous one by providing the ability to statically link custom native code along into the host executable. This involves:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would love that option. It would solve quite a lot of issues for us in deployment mode.

@swaroop-sridhar
Copy link
Contributor Author

swaroop-sridhar commented Feb 8, 2019

how do I select which native deps I'll bundle with each?

Yes @ayende Single-file apps are platform-specific. So, they must be generated from a platform specific publish (wrt a particular RID), whether framework-dependent or self-contained.

Embedding the correct native dependencies into a single-file is no different from copying the native dependencies to the RID-specific publish directory. The single-exe part of the build only embeds the contents of the RID-specific publish directory into the host.

Are you asking about adding additional native binaries that are not in the publish directory?

@MichalStrehovsky
Copy link
Member

We might need to define what APIs like Assembly.Location and Assembly.CodeBase return for bundled assemblies. This is a problem that has been plaguing CoreRT/ProjectN for a long time (see e.g. dotnet/corefx#35148 (comment), dotnet/corert#5647, dotnet/corert#6679 (comment), dotnet/corert#6947, dotnet/corert#6946).

I haven't been able to find a solution that would work for everyone and the best I have is for us to offer a new AppContext switch to select one of these behaviors (one of them should be the default):

  • Location returns an empty string. This is a documented behavior of Location if the assembly got loaded from byte array. Theoretically, user code should be able to handle that. In reality, this is a very good way to break everyone using this API.
  • Location returns the name of the assembly, without a file path. This should work for people who want to roundtrip the location through e.g. Assembly.Load.
  • Location returns the name of the assembly, qualified by AppContext.BaseDirectory. This makes existing code that does Path.GetFilePath(Assembly.GetEntryAssembly().Location) to get to the app's directory. This pattern is extremely common.

@ViktorHofer
Copy link
Member

ViktorHofer commented Feb 8, 2019

I don't have much context here but naively speaking, couldn't option 3 (AppContext.BaseDirectory + assembly name) also allow round-tripping with Assembly.Load? Means if the full path passed into Assembly.Load is the current executing assembly it won't attempt to load that assembly and just returns.

@jkotas
Copy link
Member

jkotas commented Feb 8, 2019

This is a problem that has been plaguing CoreRT/ProjectN for a long time

I think the large part of the problem has been that CoreRT/ProjectN were niches and not many folks cared to make their projects compatible with the "single file" model used by them. The good news is that making "single file" model more first class citizen will make more folks to care about it.

We are going to need new APIs to allow apps and libraries to do what they need to do in the (bundled) single file mode. It is unlikely that we would be able to make things "just work" well by quirking existing APIs. I would worry about making sure that we have all the right API first, and only worry about quirking existing APIs last based on the feedback.


Since all the files of an app published as a single-file live together, we can perform the following optimizations

- R2R compile the app and all of its dependent assemblies in a single version-bubble
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this at odds with the other goal of "Able to generate cross-platform bundles (ex: publish a single-file for Linux target from Windows)"? My understanding is that at present, we do not support "cross-crossgen", correct?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for pointing that out @nguerrera. Yes, we can generate cross-platform bundles without any new cross-compilation optimizations, until cross-crossgen is available.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@swaroop-sridhar is there a "not" missing from the second sentence? Is it that we cannot until we have cross-crossgen?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's a typo, sorry.


```xml
<ItemGroup>
<SingleFileAdd Include="List of file paths"/>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are IDE considerations for adding new item types. This may not give the best experience for maintaining such files in the IDE.

We should consider if this can be metadata on Content. Today there is CopyToOutputDirectory, CopyToPublishDirectory. There can be IncludeInSingleFilePublish or something.

Today, CopyToOutputDirectory implies CopyToPublishDirectory by default and you can override.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a good suggestion @nguerrera, I'll change SingleFile Include/Exclude to be meta-data.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nguerrera I've updated the doc now to use IncludeInSingleFilePublish metadata.


* Publish directory contains the host `HelloWorld.exe` , the app `HelloWorld.dll`, configuration files `HelloWorld.deps.json`, `HelloWorld.runtimeconfig.json`, and the PDB-file `HelloWorld.pdb`.

* Single-file publish: `dotnet publish /p:PublishSingleFile=true`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should PublishSingleFile change the default output directory of the publish? Incremental publish today already has some problems. I suspect publishing alternating between single-file and not will compound them if we're not careful. We might also take the opportunity to spec out the incrementality better, fix bugs, and handle this case carefully.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or we could not advertise using a global swtich on publish and instead recommend setting PublishSingleFile in project file based on configuration. In general, switching global properties between msbuild invocations is tricky business with respect to incrementality.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The output directory issue is should be handled similarly for all post-processing tools (currently ILLinker, crossgen, and single-file). The publish process may temporarily use other directories, but ultimately, the publish directory should contain the single-file, I think.

I agree with the idea of marking the PublishSingleFile on the project file rather than the command line, in order to reduce confusion wrt incremental build.


* Publish directory contains the host `HelloWorld.exe` , the app `HelloWorld.dll`, configuration files `HelloWorld.deps.json`, `HelloWorld.runtimeconfig.json`, and the PDB-file `HelloWorld.pdb`.

* Single-file publish: `dotnet publish /p:PublishSingleFile=true`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We may want to note the option to add dotnet publish --single-file as sugar for dotnet publish /p:PublishSingleFile=trure . Whether this is a good idea depends on how we answer my other questions above.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know it seems really minor, but IMHO having CLI sugar for this would be really nice in terms of lightweight usage, demos, discoverability, etc.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there just the single switch?

Agree that we want some CLI love on this. Want to design it with the full richness we'll need later.

@MichalStrehovsky
Copy link
Member

We are going to need new APIs to allow apps and libraries to do what they need to do in the (bundled) single file mode. It is unlikely that we would be able to make things "just work" well by quirking existing APIs.

Not having tools available to help existing code bridge the gap until it can move to the new API is always a source of sadness for the customer, and for the person trying to help the customer. I'm not as enthusiastic about libraries adapting to this quickly. Single file deployments will be niche, unless we make them the default.

I had conversations like this many times, and some of them were literally about Assembly.Location:

- Just don't call this API, call this other new API instead.
- The call is in a NuGet package.
- Oh.

@jeffschwMSFT
Copy link
Member

@MichalStrehovsky thanks for bringing this perspective. The good thing is that in these cases these files could be spilled to disk. In general we strive to enable the spill to disk with high fidelity to the original apps intention.

@ericsampson
Copy link

Is someone from Azure App Service looped in this design discussion, because it could play in very nicely with Run From Package (which is also used by Azure Functions, so coldstart perf improvements if the 'extract once' functionality is leveraged automatically? Or eventually the Stage 4/5 'extract and run in-mem' could be huge?) @odhanson @jeffhollan

@swaroop-sridhar
Copy link
Contributor Author

Thanks @ericsampson. That's a great idea.
@KetanChawda-MSFT can you please tag the team that handles "Run from package" app service, to take a look at this design? Thanks.

@MichalStrehovsky
Copy link
Member

The good thing is that in these cases these files could be spilled to disk

They'll be spilled to the temporary location so the code that assumes it can read e.g. a config file from Path.GetFilePath(Assembly.GetEntryAssembly().Location) is still going to get some path in %TEMP%. We might as well return the same string we would return for the embedded case so that the implementation detail of extracting to a temporary location won't be visible (especially if we're extracting due to a current implementation limitation and not because the user asked for that).

It's not an academic scenario - we see this pretty often in ProjectN. Here's one in xunit.

@jeffschwMSFT
Copy link
Member

jeffschwMSFT commented Feb 8, 2019

@MichalStrehovsky why would we hide the paths? In general being single-exe is a chosen path and spilling to disk is also a choice. I would opt for defaults that enable the most things to 'just work'. I would presume that if we spill to disk and not hide the path that most uses of Location, etc. would just work. Are there cases that do not?

@MichalStrehovsky
Copy link
Member

MichalStrehovsky commented Feb 9, 2019

I would presume that if we spill to disk and not hide the path that most uses of Location, etc. would just work. Are there cases that do not?

The cases when the assembly is not interesting, only the directory where the assembly is. Path.GetDirectoryName(Assembly.GetEntryAssembly().Location) is a common way to get to the "app directory" to read config files or load plugins from. Doing that is roughly equivalent to calling AppContext.BaseDirectory right now. If we do nothing, it will either be an empty string (loaded from memory), or a path to the extracted files, depending on implementation details (that might change).

We either come up with a standardized way to approach this (e.g. single file deployments on Xamarin seem to prefer returning directory-unqualified assembly file name), or every single implementation is going to have their own way to do this, breaking user code in a different way (CoreCLR is not the first runtime to have single-file deployments in .NET). There should be just a single way to do this in .NET Core.

Copy link

@poke poke left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One question I had that wasn’t answered: When extracting the files to a temporary or permanent location, writing to that location is an unsupported action, right? E.g. if I have an application with embedded configuration, and that configuration file then gets extracted somewhere, I should probably not edit that file but rather store the edited one somewhere else (e.g. next to the host application).


In .Net Core 3.0, we plan to implement a solution that

* Is widely compatible: Apps containing MSIL assemblies, ready-to-run assemblies, native binaries, configuration files, etc. can be packaged into one executable.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel like configuration shouldn’t be included in this list by default. If I think about a common use case for this single-file system, then I am thinking of easy-to-distribute utility applications that likely come with a default configuration setup and can be further configured by a file that’s next to the application. If the configuration file gets embedded by default, then there needs to be additional logic to support an external configuration file (outside of the bundle).
But if the configuration file does not get embedded, this can be simply handled as the file being optional (this would be very easy to implement with M.E.Configuration).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem is how you define "configuration". What is configuration for you may not be a configuration for me. As mentioned elsewhere in the doc, you will be able to customize what to include in the bundle.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

By configuration, here I mean .net-core's configuration files which are recognized and handled by the .net-core host/runtime. All other files are data-files from the point of view of the runtime, and should be handled as such.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

E.g. ASP.NET comes with bunch of its own configuration .json files that are just data-files from the point of the runtime.

I do not think the distinction between data files and configuration files matters much. All publishes stuff will get bundled (correct?) by default, it does not really matter what it is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes that's correct, Whatever is in the publish directory will get bundled, other files can be included too, as you mentioned above. Thanks.


* Normal publish: `dotnet publish`

* Publish directory contains the host `HelloWorld.exe` , the app `HelloWorld.dll`, configuration files `HelloWorld.deps.json`, `HelloWorld.runtimeconfig.json`, and the PDB-file `HelloWorld.pdb`.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don’t framework-dependent publishes only include the app’s .dll and to run it, you have to run dotnet HelloWorld.dll? Where does this .exe come from?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That was changed for .NET Core 3.0. You get .exe for framework dependent publishes by default in .NET Core 3.0.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh cool, didn’t know that! So the .exe will then pick up the “correct” runtime if one is installed automatically? Do you happen to have a link for me where this process is described?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can find most of this process described here


The above design should be extended to seamlessly support single-file publish for plugins.

- The bundler tool will mostly work as-is, regardless of whether we publish an application or class-lib. The binary blob with dependencies can be appended to both native and managed binaries.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What happens when plugins require a common dependency that would already be there with the application? Would they have to bundle it again?
For example, let’s assume application Foo.exe uses a library Foo.Common.dll. Now, plugins can be built that use the Foo.Common.dll. The application then has some mechanism to pick these up.

With the classic approach, or even .NET Framework, the application might have exactly those two outputs: Foo.exe and Foo.Common.dll. And when you create the plugin, you might end up with plugin.dll and Foo.Common.dll. Now since the common library would already be bundled with the application, distributions of the plugin would only have to contain the plugin.dll. You could paste that into the application folder, enable the plugin, and it would work.

With .NET Core and single-file publishing, this would now mean that both the application and the plugin would bundle Foo.Common.dll and all the runtime dlls into their final output. So you have those twice for this setup. Of course, you could opt in to make the library not bundle into a single file, but the plugin might also have some other dependencies, so it likely would want to bundle into a single file. So in the end, this would require a lot of configuration for the plugin author to produce an efficient output.

Do you have any thoughts on this situation?

Copy link
Member

@jkotas jkotas Feb 9, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This problem of what dependencies to include with plugins is not specific to single .exe deployment. You are right that one needs to be aware what comes with the app vs. what needs be included with the plugin.

Take a look at plugin sample https://github.com/dotnet/samples/tree/master/core/extensions/AppWithPlugin that shows how it can be done.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree, the goal of this work is to just package the contents of the publish directory to a single file. It shouldn't change the contents of the publish directory. The way to resolve missing dependencies for a plugin depends on it's configuration and isolation settings.

For example, if we didn't package Foo.Common.dll with plugin.dll, and plugin.dll is loaded in it's own AssemblyLoadContext, then it could:

  • Choose to not load Foo.Common.dll in its own ALC, in which case, the load-logic falls back to the default ALC (of Foo.exe) and loads Foo.Common.dll in that ALC.
  • Choose to explicitly load Foo.Common.dll from the Foo.exe bundle, using GetFileStream API proposed here.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This problem of what dependencies to include with plugins is not specific to single .exe deployment.

The difference for non single file deployments is that you can simply diff what files would be added by your plugin and only distribute those.

[…] the load-logic falls back to the default ALC (of Foo.exe) and loads Foo.Common.dll in that ALC.

So it would work if you deploy the app as a single file and choose not to do that with the plugin (distributing only the plugin.dll), and the plugin.dll could then still (automatically) access the common library and (more importantly) the framework dependencies from the Foo.exe?

The only problem I would see if the plugin has additional dependencies and wants to do a single-file deployment of only those dependencies that are specific to the plugin (and aren’t already included in the app). But I guess this would work with customization of the bundling behavior, so all the framework assemblies are not bundled.

On that note, I could actually imagine just bundling the framework assemblies for every single self-contained application, so that there’s only the .exe, the concrete dependencies, and then maybe a framework.dll. – Would that work somehow or would that already be problematic because the executing assembly needs to be a host file that is able to unpack the files?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@poke : A few points to clarify:

  • The plugin and app should be developed with a proper expectation of each other's dependencies (native binaries and assemblies).
  • If diffing published files is desirable, we still have the option to publish without the single-file option. The dependencies should not be different whether we publish single-file or not.
  • Framework is typically never packaged with plugins.
  • The runtime will provide call-backs extract file embedded within plugins. We may be able to refactor the host code such that the callbacks may reuse that code for handling single-file plugins.

* Be short, to reduce the risk of running over path length limit.
* Be randomized, in order to support concurrent launches of the same app.

The proposed extraction path is `%TEMP\.net\<random-code>` / `$TMPDIR/.net/<random-code>` .
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would this be configurable? I could imagine that some applications might rather have this be %TEMP%\MyCompany\<random-code>.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The .net part just signifies that these are temporary files created by .net-core. This way, the entire .net folder can be easily cleaned up if necessary. We could, of course, have %temp%\.net\app\... etc. But we choose shorter file names -- so that we don;t hit the max-path limit.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I do understand the idea behind this but I could still see some situations where having a .net in the path would be unwanted. The fact that the application is a .NET application is an implementation detail after all.

* Uninstall: Users can identify and delete extracted files when the app is no longer needed.
* Access control: Processes running with elevated access can extract to admin-only-writable locations.

At first startup, the host takes a disk lock (in order to handle concurrent launches), extracts files to an *install-location*, verifies them, and writes and *end-marker* before releasing the lock. If the host fails to extract all the resources at startup, it will attempt to clean up all contents of the install-location before termination.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Typo: ”writes an end-marker


A different extraction location may be specified via a `runtimeconfig` option.

The cleanup of extracted files in the install-location will be manual in this version. We can consider adding `dotnet CLI` commands for cleanup in future. However, future versions are expected to spill fewer artifacts to disk, making the cleanup commands a lower priority feature.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For self-contained apps, users are not expected to have a dotnet CLI, so there might be some other approach needed to clean it up. Would it be possible for the app to clean up itself, to implement some “Exit and uninstall” functionality into the application?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean not cleanup on every exit, but on a specific uninstall run, right? Yes, the app-developers can implement this, because the install-location of extracted files is predictable.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, using a persistent location (so that the extracting only happens once) but at the same time offering an option within the application to remove the extracted files at that location.


* Reuse: Extract on first-run, reuse on subsequent runs.
* Upgrade: Each version of the app extracts to a unique location, supporting side-by-side use of multiple versions.
* Uninstall: Users can identify and delete extracted files when the app is no longer needed.
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In extract.md, the temporary extraction was explained to clean up the temporary location on exit, so how does this work with this explanation where there the extracted files are reused?

And how exactly would users be able to identify no longer needed files? Especially when running an application through multiple versions, I would imagine that it would be quite difficult for users to identify which version they are running and which files they can delete (especially since this would still be a manual process).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removal of persistent-extracted files will be manual in this version, as noted here. That is, user can look for the app in $HOME/.dotnet/Apps/<AppName> and delete the apps he doesn't want to use anymore.

Here, versioning is based on a hash-of the entire single-file (including assemblies, binary and data files, signatures etc). That is, if the app, or any of its dependencies changed when recompiled, we will extract to a new location. So, it is tricky to provide an accurate human-readable description generically for all apps.

However, we can provide some information to help this situation. For example, as part of the end-marker, we can write out the full name and assembly version of the app's main assembly (or version of all assemblies in the app).

* MSIL files bundled into the single-file will load and execute directly from the executable.
* Native libraries will still need to
* Remain in the publish directory unmerged, or
* Extracted to the disk like the previous self-extractor stage
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What about an option that combines these two approaches? I.e. extract the native libraries to the current folder on first launch (and whenever they get deleted).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This option can be achieved by using persistent-extraction, and setting the extraction-location as the current directory, using the configuration mentioned here.

@ayende
Copy link

ayende commented Feb 9, 2019

WRT Location, what about /home/me/app.bundle;app.dll ? That would still maintain the get the base direction method, and it is also clear that this is something new.

@ayende
Copy link

ayende commented Feb 9, 2019

About config files, etc.
I can tell you what we would do in this case. We currently have a settings.default.json file that we bundle with our distribution. At startup, we search first for settings.json in the base directory and copy the settins.default.json file.

We'll absolutely need that to be in the same directory as the bundled app for discovery to the user.

On the other hand, I can certainly see the need for throwing some data in such an bundled app somewhere that isn't clearly visible (~/.myapp) works great and there are already APIs for it.

@swaroop-sridhar
Copy link
Contributor Author

@MichalStrehovsky I added the discussion about Assembly.Location here.
I think Assembly.Codebase can remain unchanged for bundled or normal assemblies.

This checkin has two changes:
* Add information about collecting telemetry
* Clarify the information about implementation repos.
Remove the bundler command line interface, since it is not expected to be used by customers.
Add a note about how SDK consumes the bundler tool.

* [Mono/MkBundle](https://github.com/mono/mono/blob/master/mcs/tools/mkbundle)
* [Fody.Costura](https://github.com/Fody/Costura)
* [Warp](https://github.com/dgiagio/warp)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@nsentinel
Copy link

It may make sense to look at the https://github.com/MiloszKrajewski/LibZ design with similar goals for the .NET Framework.

We have used it for a long time without any issues in many projects (thanks to @MiloszKrajewski)

@swaroop-sridhar
Copy link
Contributor Author

Thanks for the reference @nsentinel.
The core of LibZ is to embed assemblies as resources, and instrumenting the app to use resolvers to load the assemblies from resources. LibZ also adds a container abstraction to reduce assembly size.
I'll add LibZ to the list of related work. Thanks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.