Forum Xamarin.Mac

How to add Content files from referenced assembly in app bundle

SergSerg RUMember ✭✭
edited July 28 in Xamarin.Mac

I have Xamain.Mac project named MainApp (Xamarin Mac Full 4.8), which references the Class Library project named Lib (Net FW 4.8).
The Lib project have the txt file ContentFile.txt with Build Action = Content and Copy to output directory = Always. The ContentFile.txt contains some data, needed by Lib to work.

When building the solution, I expect that ContentFile.txt will be included in Application bundle (MainApp.app).
But actually I am observing following behaviour:

  • ContentFile.txt is present in the Lib project output folder (/Lib/bin/Debug/ContentFile.txt) along with the Lib.dll
  • ContentFile.txt is present in the MainApp project output folder (/MainApp/bin/Debug/ContentFile.txt) along with the Lib.dll and MainApp.exe
  • ContentFile.txt is not present in the MainApp.app application bundle (/MainApp/bin/Debug/MainApp.app). But Lib.dll is present inside the bundle (so, looks like the build system determined Lib.dll dependency correctly).

So, the question is - how to configure solution to include Content files from inside referenced projects to the main application bundle? Ideally, without any explicit references to these content files outside the Lib project.

What I already tried, but all of this looks not very good:

  • add explicit reference to ContentFile.txt in the MainApp project with Build action = bundle resource.
  • extract ContentFile.txt in the separate Shared project, then reference this shared project it both Lib and MainApp projects.
  • use postbuild events in MainApp project to copy file.

All of this worked, but required to 'leak' internal information from inside Lib project and I wonder if the better solutions are exist.

Additional note: the Lib project is also used in Windows version of my app, so I can't change Lib project in any way that will make it incompatible with windows.

Best Answer

  • SergSerg RUMember ✭✭
    edited July 30 Accepted Answer

    Thanks for the references! After a few hours of experiments I got the working solution. Here we go.

    I created CustomBuildActions.targets file in the Lib folder with the following contents

    <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    
        <!--
        Define project to get content files from.
        Definition relies on the fact that this target file stored
        in the same folder with Lib.csproj
        -->
        <ItemGroup>
            <LibProject Include="$(MSBuildThisFileDirectory)/Lib.csproj"/>
        </ItemGroup>
    
        <!--
        Run msbuild for Lib to collect the list of Content files and store it to the LibContentFiles list.
        Then perform string repace to convert paths to Content files to paths inside app bundle. And store results in the LibContentFileTargetPath.
        -->
        <Target Name="GetBundleFiles" Outputs="@(LibContentFiles)">
            <MSBuild Projects="@(LibProject)" Targets="ContentFilesProjectOutputGroup">
                <Output ItemName="LibContentFiles" TaskParameter="TargetOutputs"/>
            </MSBuild>
    
            <ItemGroup>
                <LibContentFileTargetPath Include="@(LibContentFiles->Replace($(MSBuildThisFileDirectory), $(AppBundleDir)/Contents/Resources/))"/>
            </ItemGroup> 
    
            <Message Text="@(LibContentFiles)"/>
            <Message Text="11111"/>
            <Message Text="@(LibContentFileTargetPath)"/>
    
        </Target>
    
        <!-- These targets will fire after mmp creates your bundle but before code signing -->
        <PropertyGroup>
            <CreateAppBundleDependsOn>$(CreateAppBundleDependsOn);GetBundleFiles;CopyOurFiles;</CreateAppBundleDependsOn>
        </PropertyGroup>
    
        <!-- Since this has inputs/outputs, it will fire only when the inputs are changed or the output does not exist -->
        <Target Name="CopyOurFiles" Inputs="@(LibContentFiles)" Outputs="@(LibContentFileTargetPath)">
            <Message Text="This is us copying a file into resources!" />
            <!-- This could have easily been done w/ a built in build action, but you can extend it arbitrary. -->
            <Copy SourceFiles="@(LibContentFiles)" DestinationFiles="@(LibContentFileTargetPath)" />
    </Target>
    </Project>
    

    Then import this target in the MainApp project

      <Import Project="../Lib/CustomBuildActions.targets" />
    

    It is also required to take into account the different paths of content files relatively of Lib.dll on Mac (../Resources/contentFile.txt) and Window (./ContentFile.txt) when working with them in Lib code.

    It is also a good idea to include the information about importing the additional target in the exception's message in case when some of Content file not found.

    And that's all. The custom target will collect the Content files from Lib project, transform the paths to preserve folder structure inside app bundle (e.g. if we will have inside Lib project something like Lib/ContentFolder/Subfolder/contentFile.txt, then inside the bundle if will be placed in Resources/ContentFolder/Subfolder/contentFile.txt) and then copy files inside the bundle.

    All logic, change history and so on are localized in the Lib folder. In case when Content files added or removed, there is no need to change custom targets.

Answers

  • ChrisHamonsChrisHamons USForum Administrator, Xamarin Team Xamurai

    So I can point you to the specific msbuild hooks to make copying files into your App Bundle easier, but they are inherently changes you make on the application side of the house.

    If you are trying to avoid that, I can think of only two approaches:

    • Convert your library into a full blown nuget. Nugets have a mechanism to inject arbitrary msbuild into consumer projects, so you can use that to get your file included. Building nugets are not the most lightweight solution, but this would totally work.
    • Drop embedding a separate file, and embed it directly in your library. You can have your library embed the required file directly. See https://stackoverflow.com/questions/3314140/how-to-read-embedded-resource-text-file for details.
  • SergSerg RUMember ✭✭

    Thank for your advices!

    Yes, nuget looks like an overkill here. Not only it is hard to build, it will also require additional infrastructure to host and additional configuration to install (from source other that nuget.org).

    The embedded resource is a good variant, but in my particular case I have executables in the list of Content. But embedded resource can't be executed. So, embedding is not usable for my current task.

    And in case when no better solutions present, the msbuild-based approach looks not so bad.
    Ideally, it may be the custom target file (which will be imported in MainApp project or any other xamarin-based consumers) which will dynamically collect information from Lib project about Content items and then add them as BundleResources in main project.
    For now I do not know if it is possible with msbuild to get list of items from Lib project during the building the MainApp project. Will additionally research this tomorrow, but any related information will be appreciated.

    Any other advises/links/etc about msbuld-based approach will be also appreciated.

  • ChrisHamonsChrisHamons USForum Administrator, Xamarin Team Xamurai

    The msbuild approach has a sample here

    But roughly you want something like at the end of the csproj in question

        <PropertyGroup>
            <CreateAppBundleDependsOn>$(CreateAppBundleDependsOn);CopyOurFiles</CreateAppBundleDependsOn>
        </PropertyGroup>
    
        <!-- Since this has inputs/outputs, it will fire only when the inputs are changed or the output does not exist -->
        <Target Name="CopyOurFiles" Inputs="xamagon.png" Outputs="$(AppBundleDir)/Contents/Resources/xamagon.png">
            <Copy SourceFiles="xamagon.png" DestinationFiles="$(AppBundleDir)/Contents/Resources/xamagon.png" />
        </Target>
    

    Let me know if you have questions, I'll point you to the relevant msbuild documentation.

  • SergSerg RUMember ✭✭
    edited July 30 Accepted Answer

    Thanks for the references! After a few hours of experiments I got the working solution. Here we go.

    I created CustomBuildActions.targets file in the Lib folder with the following contents

    <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    
        <!--
        Define project to get content files from.
        Definition relies on the fact that this target file stored
        in the same folder with Lib.csproj
        -->
        <ItemGroup>
            <LibProject Include="$(MSBuildThisFileDirectory)/Lib.csproj"/>
        </ItemGroup>
    
        <!--
        Run msbuild for Lib to collect the list of Content files and store it to the LibContentFiles list.
        Then perform string repace to convert paths to Content files to paths inside app bundle. And store results in the LibContentFileTargetPath.
        -->
        <Target Name="GetBundleFiles" Outputs="@(LibContentFiles)">
            <MSBuild Projects="@(LibProject)" Targets="ContentFilesProjectOutputGroup">
                <Output ItemName="LibContentFiles" TaskParameter="TargetOutputs"/>
            </MSBuild>
    
            <ItemGroup>
                <LibContentFileTargetPath Include="@(LibContentFiles->Replace($(MSBuildThisFileDirectory), $(AppBundleDir)/Contents/Resources/))"/>
            </ItemGroup> 
    
            <Message Text="@(LibContentFiles)"/>
            <Message Text="11111"/>
            <Message Text="@(LibContentFileTargetPath)"/>
    
        </Target>
    
        <!-- These targets will fire after mmp creates your bundle but before code signing -->
        <PropertyGroup>
            <CreateAppBundleDependsOn>$(CreateAppBundleDependsOn);GetBundleFiles;CopyOurFiles;</CreateAppBundleDependsOn>
        </PropertyGroup>
    
        <!-- Since this has inputs/outputs, it will fire only when the inputs are changed or the output does not exist -->
        <Target Name="CopyOurFiles" Inputs="@(LibContentFiles)" Outputs="@(LibContentFileTargetPath)">
            <Message Text="This is us copying a file into resources!" />
            <!-- This could have easily been done w/ a built in build action, but you can extend it arbitrary. -->
            <Copy SourceFiles="@(LibContentFiles)" DestinationFiles="@(LibContentFileTargetPath)" />
    </Target>
    </Project>
    

    Then import this target in the MainApp project

      <Import Project="../Lib/CustomBuildActions.targets" />
    

    It is also required to take into account the different paths of content files relatively of Lib.dll on Mac (../Resources/contentFile.txt) and Window (./ContentFile.txt) when working with them in Lib code.

    It is also a good idea to include the information about importing the additional target in the exception's message in case when some of Content file not found.

    And that's all. The custom target will collect the Content files from Lib project, transform the paths to preserve folder structure inside app bundle (e.g. if we will have inside Lib project something like Lib/ContentFolder/Subfolder/contentFile.txt, then inside the bundle if will be placed in Resources/ContentFolder/Subfolder/contentFile.txt) and then copy files inside the bundle.

    All logic, change history and so on are localized in the Lib folder. In case when Content files added or removed, there is no need to change custom targets.

Sign In or Register to comment.