Create .NET 4 ClickOnce applications from the command line

ClickOnce packages have always been complicated to build from the command line with if you want to go beyond msbuild /t:Publish. Given that ClickOnce doesn’t seem as common these days, there’s a dearth of information regarding building .NET 4 applications, so I’ll record some of the issues I encountered in the hope of saving others time.

  1. Use the correct version of mage.exe: This is located in the Windows SDK (Program Files\MicrosoftSDKs\Windows\7.0A or 7.1) under ‘bin\NETFX 4.0 Tools’. Don’t use the mage.exe directly under bin – this is the 3.5 version and you’ll end up getting “Assembly is incorrectly specified as a file” errors.
  2. Select ‘Create application without a manifest’ under the executable project properties Application tab: If you embed a manifest ClickOnce seems to get confused and you’ll get “Reference in the manifest does not match the identity of the downloaded assembly”.
  3. Untick the ‘Enable ClickOnce security settings’ checkbox in the project properties Security tab: This also causes issues if you are building external to Visual Studio.
  4. Specify the ‘AppCodeBase’ parameter on -New Deployment: I found if the AppCodeBase is not specified, it defaults the value specified by AppManifest but mangles (truncates) it.
  5. Add .deploy extension to files after -New Application: you’ll need a .deploy extension on the files to serve them from IIS, but if you add this before creating the application manifest, you’ll get “mismatched identity, expected file name: ‘<filename>.deploy>” warnings in the build. You need to create the manifest, change the filename, then re-sign the manifest.

You’ll also need to modify your deployment manifest xml directly – you can’t set all the values via mage.exe (in particular mapFileExtensions). It will need to be re-signed afterwards.

Below is listed a sample psake build script that generates a ClickOnce package. It assumes the correct mage is available on the path, and the certificate doesn’t have a password.

Task Build {
    #Build your solution...

    $files = "$build_artifacts_dir\Release\MyApp", "$build_artifacts_dir\Release\MyApp.exe.config"
   
    Create-ClickOnce $files -App_Name "MyApp" -Version "1.0.0.0" -Output_Dir "$build_artifacts_dir\ClickOnce" -Cert $certificate -Deployment_Url "http://example.com/MyApp"
}

function Create-ClickOnce {
    param($app_name, $version, $output_dir, $cert, $deployment_url) 
    $files = $args[0]
    
    $version_dir = "$output_dir\$app_name_$($version.Replace(".", "_"))"
    mkdir $version_dir
    $relative_version_dir = [System.IO.Path]::GetFileName($version_dir)

    #Copy files into the output folder and generate the .manifest and .application files.
    Copy-Item $files -Destination $version_dir
    Exec {mage -New Application -ToFile "$version_dir\$app_name.exe.manifest" -Name $app_name -Version $version -Processor X86 -CertFile "$cert" -FromDirectory $version_dir -TrustLevel FullTrust }
    Exec {mage -New Deployment -ToFile "$output_dir\$app_name.application" -Name $app_name -Version $version -Processor X86 -AppManifest "$version_dir\$app_name.exe.manifest" -AppCodeBase "$deployment_url/$relative_version_dir/$app_name.exe.manifest" -CertFile $cert -IncludeProviderURL true -ProviderURL "$deployment_url/$app_name.application" -Install true }
    
    #Append .deploy to files for web server deployment, then re-sign the manifest. No idea why mage can't do this.
    Get-ChildItem $version_dir | Foreach-Object { if (-not $_.FullName.EndsWith(".manifest")) { Rename-Item $_.FullName "$($_.FullName).deploy" } } 
    Exec {mage -Sign "$version_dir\$app_name.exe.manifest" -CertFile $cert }

    #Set parameters in deployment xml, then re-sign the deployment. Why can't we do this from the command line, Microsoft?
    $xml = [xml](Get-Content "$build_artifacts_dir\ClickOnce\NaplanDB.application")
    $deployment_node = $xml.SelectSingleNode("//*[local-name() = 'deployment']")
    $deployment_node.SetAttribute("minimumRequiredVersion", $version)
    $deployment_node.SetAttribute("mapFileExtensions", "true")
    $xml.Save("$output_dir\$app_name.application")
    Exec {mage -Sign "$output_dir\$app_name.application" -CertFile $cert }
}
Advertisements