1
votes

We have a ClickOnce application which we're trying to get running with CI/CD on Azure DevOps.

Currently the ClickOnce Prerequisites are set to 'Download prerequisites from the same location as my application' which is the behaviour we want ideally (this app is installed on a Customer's server and the client PCs download the .NET framework and various custom bootstrappers from that server):

ClickOnce prerequisites screen showing install location preference set

When VS builds on my local development PC it picks up those bootstrapper files (which includes some custom bootstrappers we've written) from the local machine and outputs them to the ClickOnce Publishing folder:

Output from ClickOnce publish in Windows Explorer

However, this doesn't work on Azure DevOps I get this error about not being able to find the .NET bootstrapper instead:

[error]C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\Microsoft.Common.CurrentVersion.targets(5390,5): Error MSB3152: To enable 'Download prerequisites from the same location as my application' in the Prerequisites dialog box, you must download file 'DotNetFX462\NDP462-KB3151800-x86-x64-AllOS-ENU.exe' for item 'Microsoft .NET Framework 4.6.2 (x86 and x64)' to your local machine. For more information, see http://go.microsoft.com/fwlink/?LinkId=616018.

As you'd expect, if I untick .NET 4.6.2 as I prerequisite I no longer get an error about .NET, but weirdly I don't then get an error for our custom bootstrappers, even though those don't exist on Azure either. The difference there seems to be that those custom bootstrappers don't currently exist on my local PC either, so they are shown with yellow exclamation triangles on my local machine.

So, is the .NET bootstrapper definitely not in the Azure build server image that gets spun up, or is it perhaps just a different path?

Or failing that, is there any way to tell DevOps to ignore this issue and just carry on building the actual ClickOnce application files and complete the build? The bootstrappers are already installed on our customer's server, so I don't actually need Azure to bundle them in the output.

1
Have you figured this out?Brian Hanf
@BrianHanf, no I'm afraid not, for now I've just unticked the 4.6.2 prerequisite to get the build working.tomRedox

1 Answers

1
votes

Although not the best solution, the following worked for me:

  1. I created an artifacts feed
  2. I used Azure Artifacts with Universal packages to post my bootstrap packages
  3. In the pipeline I used the "Universal packages" task to download the packages in "$ (System.DefaultWorkingDirectory)\bootstrapper"
  4. I added the argument "/p:GenerateBootstrapperSdkPath=$(System.DefaultWorkingDirectory)\bootstrapper" to msbuild

The biggest drawback with this is the space used.

To publish the packages you will need:

  • Azure CLI
  • Azure DevOps extension for the Azure CLI: Run "az extension add --name azure-devops" when you have Azure CLI installed

Prepare the packages

In addition to your packages you need to upload the "Engine" directory which is where the setup.bin file is located. In my case I took it from "C:\Program Files (x86)\Microsoft SDKs\ClickOnce Bootstrapper"

I copied the "Engine" and "Schemas" directories to a temporary directory and then executed:

az login

az artifacts universal publish ^
    --organization https://dev.azure.com/<MY-ORGANIZATION>/ ^
    --project = "<MY PROJECT>" ^
    --scope project ^
    --feed <MY-FEED> ^
    --name clickonce ^
    --version 16.0.28315 ^
    --description "ClickOnce Bootstrapper" ^
    --path "D:\temp\ClickOnce Bootstrapper"

In the parameters "name", "version" and "description" you can put what you want, the important thing is to identify the package.

If the feed you created is associated with the organization, the command should not include the parameters "project" or "scope", you can see an example on how to publish by clicking on the "Connect to feed" button in your artifacts feed.

Then I ran the same command for each package I require, for example:

az artifacts universal publish ^
    --organization https://dev.azure.com/my-organization/ ^
    --project = "my project" ^
    --scope project ^
    --feed my-feed ^
    --name dotnet ^
    --version 4.6.2 ^
    --description "Microsoft .Net Framework 4.6.2" ^
    --path "C:\Program Files (x86)\Microsoft SDKs\ClickOnce Bootstrapper\Packages\DotNetFX462"

The pipeline

Before the compilation task add the download of the packages:

First download the engine, in the case of this example it would be the clickonce package 16.0.28315

- task: UniversalPackages@0
  inputs:
    command: 'download'
    downloadDirectory: '$(System.DefaultWorkingDirectory)\bootstrapper'
    feedsToUse: 'internal'
    vstsFeed: '<MY FEED ID>'
    vstsFeedPackage: '<MY PACKAGE ID>'
    vstsPackageVersion: '16.0.28315'

Of course, with the wizard it is much easier, find the "Universal packages" task, select the "Download" command and fill in the rest of the parameters.

For your packages it is similar, just change the download path to "$(System.DefaultWorkingDirectory)\bootstrapper\Packages<PACKAGE NAME>"

The important thing at this point is that if you already have the packages published on a website, the "Package name" is the same name of the directory where the package is currently published, since the setup will try to download it from that path.

Lastly, my compilation task looks something like this:

- task: VSBuild@1
  inputs:
    solution: '$(solution)'
    platform: '$(buildPlatform)'
    configuration: '$(buildConfiguration)'
    msbuildArgs: '/target:publish /p:GenerateBootstrapperSdkPath=$(System.DefaultWorkingDirectory)\bootstrapper'

Of course, this makes a lot more sense with your own packages. It works with the .Net Framework, but it will use some valuable MB. An alternative could be to create a script, in PowerShell for example, that downloads and installs the .Net Framework. Another option, if you have one available, is to copy the files from an FTP or fileserver, the latter applies to both the .Net Framework and its own packages, the only thing relevant is to redirect the Bootstrappers path with the parameter "GenerateBootstrapperSdkPath", about how packets get into that directory, you have several options.