briefcase icon indicating copy to clipboard operation
briefcase copied to clipboard

Add file associations to Windows and macOS installers

Open davidfokkema opened this issue 1 year ago • 8 comments

What is the problem or limitation you are having?

My application can save files containing the project you're working on. On macOS, I can double-click such a file to start my application and open it. On Windows, this works but I'll first have to manually associate my application with that type of file.

Describe the solution you'd like

Just as on macOS, I want Windows installers to install and uninstall file associations.

Describe alternatives you've considered

Briefcase uses WiX but I looked into other tools. These do not create native .msi installers so I dropped that. WiX is terrible, but it seems to be the only open-source tool to generate msi installers.

Additional context

I've managed to get it working by manually editing a few files. To fix that in Briefcase is not that easy since it touches the create, build and package stages. This is, more or less, what is needed (with my application as an example. The package step creates a file manifest using the WiX harvest tool (heat) which contains all files. The executable stub file must be removed from that manifest, along with its component and the reference to that component in the fragment at the end of the manifest. However, the File element must be included in the wxs file created in the create step. It seems to contain some kind of hash and I still need to figure out if and when that hash changes. If it does not, maybe we can choose our own hash and then it's not hard to include the line during the create step. If the hash changes based on the contents of the file created during the build step, we then need to update the .wxs file during the package step. In either case, the wxs file should include a new component for the file associations:

<Component Id="FileAssociation" Directory="tailor_ROOTDIR" Guid="*">
    <File Id="ProductIcon" Source="icon.ico" />
    <File Id="fil5E92122F5D3D3D9A85322DF825CEEBB9" KeyPath="yes" Source="$(var.SourceDir)\Tailor.exe" />
    <ProgId Id="MyApp.document" Description="Tailor Project" Icon="ProductIcon">
        <Extension Id="tlr" ContentType="application/tailor">
            <Verb Id="open" Command="Open" TargetFile="fil5E92122F5D3D3D9A85322DF825CEEBB9" Argument='"%1"' />
        </Extension>
    </ProgId>
</Component>

and that component must be included in the default feature:

<Feature Id="DefaultFeature" Level="1">
    <ComponentGroupRef Id="tailor_COMPONENTS" />
    <ComponentRef Id="ApplicationShortcuts"/>
    <ComponentRef Id="FileAssociation"/>
</Feature>

So, I see basically two options:

  1. Adjust the template to include the file associations component with a placeholder for the file ID. In the package step, create the manifest and rewrite the manifest file to cut out the stub file component and the wxs file to include the correct hash.
  2. Write a replacement for the harvest tool since we just need to crawl the directory tree and output our own hashes. Still, it might be the case that the wxs file needs to be updated to include the correct hash.

This seems to be a bit of work which would touch briefcase-windows-app-template and briefcase itself.

davidfokkema avatar Mar 23 '24 16:03 davidfokkema

Component and file Ids do not change when the file changes. The Guid does change, but that changes every run even if the file is identical. What the consequences of this are, I'm not yet sure. I thought that these values were used to track which files were changed between updates, but this behaviour seems to be useless for that purpose.

davidfokkema avatar Mar 23 '24 17:03 davidfokkema

As always, taking the time to write up an issue report clears the mind a bit. Since the file ID does not change based on the contents of the file, we can choose our own value in the app template. No need to rewrite that during the build or package step. So we only need to remove the .exe from the manifest or write our own crawler. Looking at the inscrutable list of required arguments to the harvest tool, I'd like to suggest writing our own. It's just a long list of components, directories and files.

davidfokkema avatar Mar 23 '24 20:03 davidfokkema

Thanks for those details. For the benefit of anyone potentially implementing this, can I ask you to clarify a couple of things (based on what you know):

  1. I presume the examples you've presented are for an app called "Tailor", responding to an extension .tlr, with a MIME-type of application/tailor? I want to make sure tailor isn't some magic WiX keyword :-)
  2. I don't know if you've tested this, but the implication seems to be that there must be a 1-1 correspondence between extensions and executables? i.e., I couldn't define tailor.exe as opening both .tlr and .tailor extensions, because the File entry will be repeated?
  3. Where in the overall document structure does the Component get registered? I presume it is a sibling of the main Directory entry?
  4. Regarding the specifics of the implementation: Did you look into using TargetProperty rather than TargetFile to define the Verb? I haven't tried this, but it would appear that it might be possible to define a property that has a value that points at the executable without requiring the manifest post-processing you describe.

freakboy3742 avatar Mar 24 '24 01:03 freakboy3742

Sure! To clarify:

  1. Exactly right, ;-).
  2. The File entry does not need to be repeated, you can just repeat ProgId like this:
    <Component Id="FileAssociation" Directory="tailor_ROOTDIR" Guid="*">
        <File Id="ProductIcon" Source="icon.ico" />
        <File Id="fil5E92122F5D3D3D9A85322DF825CEEBB9" KeyPath="yes" Source="$(var.SourceDir)\Tailor.exe" />
        <ProgId Id="MyApp.document" Description="Tailor Project" Icon="ProductIcon">
            <Extension Id="tlr" ContentType="application/tailor">
                <Verb Id="open" Command="Open" TargetFile="fil5E92122F5D3D3D9A85322DF825CEEBB9" Argument='"%1"' />
            </Extension>
        </ProgId>
        <ProgId Id="MyApp.legacy_document" Description="Tailor Legacy Project" Icon="ProductIcon">
            <Extension Id="tll" ContentType="application/tailor">
                <Verb Id="open" Command="Open" TargetFile="fil5E92122F5D3D3D9A85322DF825CEEBB9" Argument='"%1"' />
            </Extension>
        </ProgId>
    </Component>
    
    I've just checked that it works.
  3. It is indeed a sibling of the main Directory element, yes!
  4. Hm, interesting. I haven't tried that yet but will take a look.

davidfokkema avatar Mar 24 '24 08:03 davidfokkema

  1. Regarding the specifics of the implementation: Did you look into using TargetProperty rather than TargetFile to define the Verb? I haven't tried this, but it would appear that it might be possible to define a property that has a value that points at the executable without requiring the manifest post-processing you describe.

Holy cow, that works! No need to change the manifest, you can do this:

<SetProperty Id='FileAssociationProperty' Value="[tailor_ROOTDIR]\Tailor.exe" After="CostFinalize"/>

<Component Id="FileAssociation" Directory="tailor_ROOTDIR" Guid="*">
    <File Id="ProductIcon" Source="icon.ico" />
    <ProgId Id="MyApp.document" Description="Tailor Project" Icon="ProductIcon">
        <Extension Id="tlr" ContentType="application/tailor">
            <Verb Id="open" Command="Open" TargetProperty="FileAssociationProperty" Argument='"%1"' />
        </Extension>
    </ProgId>
    <ProgId Id="MyApp.legacy_document" Description="Tailor Legacy Project" Icon="ProductIcon">
        <Extension Id="tll" ContentType="application/tailor">
            <Verb Id="open" Command="Open" TargetProperty="FileAssociationProperty" Argument='"%1"' />
        </Extension>
    </ProgId>
</Component>

So the only thing that needs changing is the app template I think. Will work on that!

davidfokkema avatar Mar 24 '24 12:03 davidfokkema