Search code examples
c#dependenciesnuget-packagefilenotfoundexceptionsevenzipsharp

Relocating dependencies for my application throws System.IO.FileNotFoundException


I'm trying to clean up my Release folder so dependency files have some type of organization to them. My goal in this case is to contain all the dependencies I need for my compression module into a single folder. For this first part of my new class library I'm just trying to support 7zip files. I do this using the SevenZipSharp.Interop NuGet which depends on the SevenZipSharp NuGet.

By default, the above NuGets build results in my Release/Debug folders and look like the following...

Release
|- x64
| `- 7z.dll
|- x86
| `- 7z.dll
|- SevenZipSharp.dll
`- compressionmodule.dll

As I add more support to my class library I want to make sure everything has it's own folder etc. So, I want to organize it in the following way...

Release
|- compressionmodule
| `- 7zip
|   |- x64
|   | `- 7z.dll
|   |- x86
|   | `- 7z.dll
|   `- SevenZipSharp.dll
`- compressionmodule.dll

I do this by running a few POST BUILD commands and once the project builds I get the structure I want above (the latter). However, when debugging, after I compile and the POST BUILD commands occur, the application tries to run and I get a System.IO.FileNotFoundException error telling me it can't find SevenZipSharp.dll. I knew this would happen in the back of my mind but was hoping there was a way to let the application know the new location of the dependencies.

Is it possible to relocate the files and then let my application know they are in a different place somehow?


Solution

  • I ended up using the NuGet package Costura.Fody (repository found here). The way it wraps up dependencies in your class module (or program) is nothing short of magic. When using Costura.Fody it will look for any dependencies and automatically compress them into you class library dll making a single dll. It will then make all references to the resource point to the embedded resources in your dll. All you need to do is add the two NuGet packages and it will do all the work when you build the project. It's really slick!

    Important Note: Fody's (a dependency of Costura.Fody needed to run Costura.Fody) current version is only compatible with Visual Studio 2019 (using MSBuild 16). In my case I have Visual Studio 2017 (which uses MSBuild 15). I had to use an old version of Costura.Fody that still used the old MSBuild. That also required me to downgrade Fody as well. So I ended up using the latest versions that would work with MSBuild 15. Fody version 4.2.1 and Costura.Fody version 3.3.3 works great with Visual Studio 2017.

    It even supports extracting the resource if needed. For example, I was looking to use this on the SevenZipSharp library. It requires a path to the 7zip.dll file to be used. So I used the following in the FodyWeavers.xml file that is located in the same directory as your solution.

    <?xml version="1.0" encoding="utf-8"?>
    <Weavers xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="FodyWeavers.xsd">
      <Costura CreateTemporaryAssemblies='true' IncludeDebugSymbols='false'>
        <Unmanaged32Assemblies>
          7z
        </Unmanaged32Assemblies>
        <Unmanaged64Assemblies>
          7z
        </Unmanaged64Assemblies>
      </Costura>
    </Weavers>
    

    You need to also include the 7zip.dll in your project so it compiles the dlls as they are not used like resources (like the SevenZipSharp library is). You do this by adding the following folders in the root of your project as well as the appropriate architectures files. Then add them as Build Action: Embedded Resource.

    enter image description here

    This would extract the 7zip.dll dependency at run-time when needed so I could reference the path when using the SevenZipSharp library. At this point I have not found out how to get a reference to the (C83815D61CE49B2E8D23145A1B95A956 maybe a checksum of all the files in the directory itself?) directory it uses but in my case this is where is was extracting it to...

    %tmp%\Costura\C83815D61CE49B2E8D23145A1B95A956\32\ enter image description here

    Update: I found a workaround to getting the path of that checksum like directory using Assembly information. I created the following function to try and set the 7Zip library path. It might be helpful to some so I'm including it here.

    private bool SetupSevenZipLibrary()
    {
        string costuraExtractionPath = null;
    
        try
        {
            //This will use the embeded resource to try and set a base path to the extraction
            //location created by by Costura.Fody
            string sevenZipAssembly = typeof(SevenZip.ArchiveFileInfo).Assembly.Location;
            if (File.Exists(sevenZipAssembly))
                costuraExtractionPath = Path.GetDirectoryName(sevenZipAssembly);
        }
        catch
        {
            //Issue trying to grab the extraction path from the Assembly information so try
            //to use the first path found in the Costura directory as a last ditch effort
    
            DirectoryInfo di = null;
    
            string costuraTempPath = Path.Combine(
                Path.GetTempPath(), //ex: C:\Users\username\AppData\Local\Temp
                "Costura" //ex: Costura
            );
    
            di = new DirectoryInfo(costuraTempPath);
            if (!di.Exists)
                return false;
            //ex: C:\Users\username\AppData\Local\Temp\Costura\C83815D61CE49B2E8D23145A1B95A956\
            costuraExtractionPath = di.GetDirectories().First().FullName;
        }
    
        try
        {
            if (!Directory.Exists(costuraExtractionPath))
                throw new Exception();
    
            string sevenZipPath = Path.Combine(
                costuraExtractionPath, //ex: C:\Users\username\AppData\Local\Temp\Costura\C83815D61CE49B2E8D23145A1B95A956
                Environment.Is64BitProcess ? "64" : "32", //ex: 32
                 "7z.dll" //ex: 7z.dll
            );
    
            if (!File.Exists(sevenZipPath))
                throw new Exception();
    
            SevenZipBase.SetLibraryPath(sevenZipPath);
            return true;
        }
        catch { return false; }
    }
    

    It's usage...

    if (!SetupSevenZipLibrary())
        throw new Exception("Error setting the path of the 7zip library.");