F# and ClickOnce - The problems I ran into and how to solve these
Recently I had a problem to deploy a F# WPF tool with ClickOnce. Yeah ClickOnce, old technology and it's still in use in some enterprises to deploy applications. The standard ClickOnce tab in the project are missing in the project template from Visual Studio. So I had to do some research and I found out, that with FAKE you can deploy a F# program via ClickOnce.
At first I want to thanks the authors of 2 blog entries...
https://fwaris.wordpress.com/2013/08/14/fake-script-for-clickonce-packaging-of-f-apps/
and
... for doing all the heavy lifting.
But I won't write a blog article about it, when this was all to do.
I run into 2 Problems:
- The FAKE Script doesn't find my MSBuild to build the project
- The Installation of the programm ends with an error: "Reference in the manifest does not match the identity of the downloaded assembly your-app.exe."
The Solutions:
1. MSBuild not found
That's an easy one. Use the following line in your FAKE-Script:
setBuildParam "MSBuild" "<your-path-to-vs>\Microsoft Visual Studio/2017/MSBuild/15.0/Bin/MsBuild.exe"
2. The Reference in the manifest does not match ...
So this one was a little harder. The solution is to not add the manifest into the assembly.
Normally in the project setting you have a drop down in the "Application" tab, which says "Create application without a manifest".
This option doesn't exist in the project settings of a F# project. So I look into a csproj file and find this following settings in the project file is there.
<PropertyGroup>
<NoWin32Manifest>true</NoWin32Manifest>
</PropertyGroup>
I add this setting to the fsproj file and it doesn't work. Damn.
After some further research I find out, that you can also add a compiler flag to avoid that the manifest is embedded into the .exe file.
--nowin32manifest
So I add this flag in the "Build" tab under "other flags:".
Now it work, as it should.
Here is the ClickOnce Deployment Script
I do not use FAKE 5, I use FAKE 4. But maybe I migrate this script to FAKE 5 in the future.
#r @".\packages\FAKE.4.64.13\tools\FakeLib.dll"
open Fake
// project and version info
let projectName = "<your-app-name-without-.exe>"
// use your own version
let version = "1.0.0.0"
// Directories
let buildDir = @".\build\"
let deployDir = @".\deploy\"
let applicationDirName = (sprintf "%s.%s" projectName version)
let publishDir = deployDir @@ applicationDirName
// here is set the ClickOnce sign certificate
let cert = @".\ClickOnce.pfx"
let certPwFile = @".\ClickOncePwd.txt"
// Set the MSBuild path
setBuildParam "MSBuild" "M:\Microsoft Visual Studio/2017/MSBuild/15.0/Bin/MsBuild.exe"
// Filesets
let appReferences = !! "<your-project-to-publish-folder>/*.fsproj"
let EmptyMageParams = {
// maybe the mage is in an other folder on your computer
ToolsPath = @"C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.7.1 Tools"
Manifest = ""
ApplicationFile = ""
CertFile = None
Name = projectName
Version = version
Processor = MSIL
TrustLevel = None
// Set here your company name
Publisher = Some "MyAwesomeCompany"
ProviderURL = ""
SupportURL = None
FromDirectory = ""
ProjectFiles = []
IconPath = ""
IconFile = ""
TmpCertFile = ""
Password = None
CertHash = None
IncludeProvider = None
Install = None
UseManifest = None
CodeBase = None }
#r "System.Xml.Linq.dll"
open System.Xml.Linq
let setUpdatePolicy (filename: string) =
let n nameSpace name = XName.op_Implicit (nameSpace + name)
let asmv1 = "{urn:schemas-microsoft-com:asm.v1}"
let asmv2 = "{urn:schemas-microsoft-com:asm.v2}"
let cov1 = "{urn:schemas-microsoft-com:clickonce.v1}"
let removeNode name (node: XElement option) =
match node with
| None -> None
| Some n ->
match n.Element(name) with
| null -> ()
| e -> e.Remove()
node
let addNode (name: XName) (node: XElement option) =
match node with
| Some n -> n.Add(new XElement(name))
| _ -> ()
let element name (node: XElement option) : XElement option =
match node with
| None -> None
| Some n ->
match n.Element(name) with
| null -> None
| e -> Some e
let doc = XDocument.Load(filename)
Some doc.Root
|> element (n asmv2 "deployment")
|> element (n asmv2 "subscription")
|> element (n asmv2 "update")
|> removeNode (n asmv2 "expiration")
|> addNode (n asmv2 "beforeApplicationStartup")
doc.Save(filename)
Target "Clean" (fun _ ->
CleanDirs [buildDir; deployDir; publishDir]
)
Target "Build" (fun _ ->
MSBuildRelease buildDir "Build" appReferences
|> Log "Release Build-Output: "
)
Target "Prepare Deployment" (fun _ ->
!! (buildDir @@ "**/*.*")
-- "**/*.pdb"
-- "**/*.xml"
|> CopyFiles publishDir
)
Target "Create ClickOnce Installer" (fun _ ->
let appManifest = publishDir @@ (sprintf "%s.exe.manifest" projectName)
let deployManifest = sprintf "%s.application" projectName
let appParams = { EmptyMageParams with Manifest = appManifest
ApplicationFile = publishDir @@ (sprintf "%s.exe" projectName)
TrustLevel = Some FullTrust
FromDirectory = publishDir
IconPath = publishDir
IconFile = projectName + ".ico"
IncludeProvider = Some false
UseManifest = Some true
CertFile = Some cert
Password = Some certPwFile}
let deployParams = { EmptyMageParams with Manifest = appManifest
ApplicationFile = deployDir @@ deployManifest
Install = Some true
CertFile = Some cert
Password = Some certPwFile }
// Create
MageCreateApp appParams
// Sign
MageSignManifest appParams
// Deploy
MageDeployApp deployParams
deployDir @@ deployManifest |> setUpdatePolicy
// Sign
MageSignDeploy deployParams
(buildDir) |> CopyFile publishDir
)
Target "Deploy" (fun _ ->
trace "Deploy the created ClickOnce installer wherever you want"
)
"Clean"
==> "Build"
==> "Prepare Deployment"
==> "Create ClickOnce Installer"
==> "Deploy"
RunTargetOrDefault "Deploy"
I hope it helps a little.