Skip to content

Commit 1c5e4d9

Browse files
authored
feat: installer and uninstaller (#32)
Adds new package `Installer` which uses [WixSharp](https://github.com/oleg-shilo/wixsharp) to build WixToolset installers for WIndows. We build two installers: - `Coder Desktop (Core)`, which installs the app files, service files, VPN files (`wintun.dll`), creates a system service and an app shortcut in the start menu - `Coder Desktop`, which will netinstall the .NET runtime if it's not installed and then chain install `Coder Desktop (Core) We will only be shipping the `Coder Desktop` installer, which contains the Core installer. The chained Core installation doesn't show up in Settings > Apps. ![image](https://github.com/user-attachments/assets/0dbcc2fb-8618-4f7e-8973-8a8226c1f3e0) ![image](https://github.com/user-attachments/assets/8a33ac0d-f2aa-4dfe-a0df-efd7da3d1d22) ![image](https://github.com/user-attachments/assets/cc5bfc38-2587-4f83-9586-c21e3d663dda) ![image](https://github.com/user-attachments/assets/6e15ed0e-273c-4dfc-99ce-22b35ef5668f) Follow-up PR will integrate to CI and add authenticode signing to the installer binaries. Closes #11 Closes #30
1 parent e1ef774 commit 1c5e4d9

38 files changed

+1801
-266
lines changed

.github/workflows/release.yaml

+69-22
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,18 @@ on:
44
push:
55
tags:
66
- '*'
7+
workflow_dispatch:
8+
inputs:
9+
version:
10+
description: 'Version number (e.g. v1.2.3)'
11+
required: true
12+
default: 'v1.2.3'
713

814
permissions:
915
contents: write
1016

1117
jobs:
12-
build:
18+
release:
1319
runs-on: windows-latest
1420

1521
steps:
@@ -20,42 +26,83 @@ jobs:
2026
with:
2127
dotnet-version: '8.0.x'
2228

29+
# Necessary for signing Windows binaries.
30+
- name: Setup Java
31+
uses: actions/setup-java@3a4f6e1af504cf6a31855fa899c6aa5355ba6c12 # v4.7.0
32+
with:
33+
distribution: "zulu"
34+
java-version: "11.0"
35+
2336
- name: Get version from tag
2437
id: version
2538
shell: pwsh
2639
run: |
27-
$tag = $env:GITHUB_REF -replace 'refs/tags/',''
40+
$ErrorActionPreference = "Stop"
41+
if ($env:INPUT_VERSION) {
42+
$tag = $env:INPUT_VERSION
43+
} else {
44+
$tag = $env:GITHUB_REF -replace 'refs/tags/',''
45+
}
2846
if ($tag -notmatch '^v\d+\.\d+\.\d+$') {
29-
throw "Tag must be in format v1.2.3"
47+
throw "Version must be in format v1.2.3, got $tag"
3048
}
3149
$version = $tag -replace '^v',''
32-
$assemblyVersion = "$version.0"
33-
echo "VERSION=$version" >> $env:GITHUB_OUTPUT
34-
echo "ASSEMBLY_VERSION=$assemblyVersion" >> $env:GITHUB_OUTPUT
50+
$assemblyVersion = "$($version).0"
51+
Add-Content -Path $env:GITHUB_OUTPUT -Value "VERSION=$version"
52+
Add-Content -Path $env:GITHUB_OUTPUT -Value "ASSEMBLY_VERSION=$assemblyVersion"
53+
Write-Host "Version: $version"
54+
Write-Host "Assembly version: $assemblyVersion"
55+
env:
56+
INPUT_VERSION: ${{ inputs.version }}
3557

36-
- name: Build and publish x64
37-
run: |
38-
dotnet publish src/App/App.csproj -c Release -r win-x64 -p:Version=${{ steps.version.outputs.ASSEMBLY_VERSION }} -o publish/x64
39-
dotnet publish src/Vpn.Service/Vpn.Service.csproj -c Release -r win-x64 -p:Version=${{ steps.version.outputs.ASSEMBLY_VERSION }} -o publish/x64
58+
# Setup GCloud for signing Windows binaries.
59+
- name: Authenticate to Google Cloud
60+
id: gcloud_auth
61+
uses: google-github-actions/auth@71f986410dfbc7added4569d411d040a91dc6935 # v2.1.8
62+
with:
63+
workload_identity_provider: ${{ secrets.GCP_CODE_SIGNING_WORKLOAD_ID_PROVIDER }}
64+
service_account: ${{ secrets.GCP_CODE_SIGNING_SERVICE_ACCOUNT }}
65+
token_format: "access_token"
4066

41-
- name: Build and publish arm64
42-
run: |
43-
dotnet publish src/App/App.csproj -c Release -r win-arm64 -p:Version=${{ steps.version.outputs.ASSEMBLY_VERSION }} -o publish/arm64
44-
dotnet publish src/Vpn.Service/Vpn.Service.csproj -c Release -r win-arm64 -p:Version=${{ steps.version.outputs.ASSEMBLY_VERSION }} -o publish/arm64
67+
- name: Setup GCloud SDK
68+
uses: google-github-actions/setup-gcloud@77e7a554d41e2ee56fc945c52dfd3f33d12def9a # v2.1.4
4569

46-
- name: Create ZIP archives
70+
- name: scripts/Release.ps1
71+
id: release
4772
shell: pwsh
4873
run: |
49-
Compress-Archive -Path "publish/x64/*" -DestinationPath "./publish/CoderDesktop-${{ steps.version.outputs.VERSION }}-x64.zip"
50-
Compress-Archive -Path "publish/arm64/*" -DestinationPath "./publish/CoderDesktop-${{ steps.version.outputs.VERSION }}-arm64.zip"
74+
$ErrorActionPreference = "Stop"
5175
52-
- name: Create Release
53-
uses: softprops/action-gh-release@v1
76+
$env:EV_CERTIFICATE_PATH = Join-Path $env:TEMP "ev_cert.pem"
77+
$env:JSIGN_PATH = Join-Path $env:TEMP "jsign-6.0.jar"
78+
Invoke-WebRequest -Uri "https://github.com/ebourg/jsign/releases/download/6.0/jsign-6.0.jar" -OutFile $env:JSIGN_PATH
79+
80+
& ./scripts/Release.ps1 `
81+
-version ${{ steps.version.outputs.VERSION }} `
82+
-assemblyVersion ${{ steps.version.outputs.ASSEMBLY_VERSION }}
83+
if ($LASTEXITCODE -ne 0) { throw "Failed to publish" }
84+
env:
85+
EV_SIGNING_CERT: ${{ secrets.EV_SIGNING_CERT }}
86+
EV_KEYSTORE: ${{ secrets.EV_KEYSTORE }}
87+
EV_KEY: ${{ secrets.EV_KEY }}
88+
EV_TSA_URL: ${{ secrets.EV_TSA_URL }}
89+
GCLOUD_ACCESS_TOKEN: ${{ steps.gcloud_auth.outputs.access_token }}
90+
91+
- name: Upload artifact
92+
uses: actions/upload-artifact@v4
93+
with:
94+
name: publish
95+
path: .\publish\
96+
97+
- name: Create release
98+
uses: softprops/action-gh-release@v2
99+
if: startsWith(github.ref, 'refs/tags/')
54100
with:
55-
files: |
56-
./publish/CoderDesktop-${{ steps.version.outputs.VERSION }}-x64.zip
57-
./publish/CoderDesktop-${{ steps.version.outputs.VERSION }}-arm64.zip
58101
name: Release ${{ steps.version.outputs.VERSION }}
59102
generate_release_notes: true
103+
# We currently only release the bootstrappers, not the MSIs.
104+
files: |
105+
${{ steps.release.outputs.X64_OUTPUT_PATH }}
106+
${{ steps.release.outputs.ARM64_OUTPUT_PATH }}
60107
env:
61108
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

App/App.csproj

+10-3
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,22 @@
1717
<LangVersion>preview</LangVersion>
1818
<!-- We have our own implementation of main with exception handling -->
1919
<DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>
20+
21+
<AssemblyName>Coder Desktop</AssemblyName>
22+
<ApplicationIcon>coder.ico</ApplicationIcon>
2023
</PropertyGroup>
2124

2225
<PropertyGroup Condition="$(Configuration) == 'Release'">
23-
<PublishTrimmed>true</PublishTrimmed>
24-
<TrimMode>CopyUsed</TrimMode>
26+
<PublishTrimmed>false</PublishTrimmed>
27+
<!-- <TrimMode>CopyUsed</TrimMode> -->
2528
<PublishReadyToRun>true</PublishReadyToRun>
26-
<SelfContained>true</SelfContained>
29+
<SelfContained>false</SelfContained>
2730
</PropertyGroup>
2831

32+
<ItemGroup>
33+
<Content Include="coder.ico" />
34+
</ItemGroup>
35+
2936
<ItemGroup>
3037
<Manifest Include="$(ApplicationManifest)" />
3138
</ItemGroup>

App/Services/CredentialManager.cs

+4-8
Original file line numberDiff line numberDiff line change
@@ -239,11 +239,9 @@ private struct CREDENTIAL
239239
public int Flags;
240240
public int Type;
241241

242-
[MarshalAs(UnmanagedType.LPWStr)]
243-
public string TargetName;
242+
[MarshalAs(UnmanagedType.LPWStr)] public string TargetName;
244243

245-
[MarshalAs(UnmanagedType.LPWStr)]
246-
public string Comment;
244+
[MarshalAs(UnmanagedType.LPWStr)] public string Comment;
247245

248246
public long LastWritten;
249247
public int CredentialBlobSize;
@@ -252,11 +250,9 @@ private struct CREDENTIAL
252250
public int AttributeCount;
253251
public IntPtr Attributes;
254252

255-
[MarshalAs(UnmanagedType.LPWStr)]
256-
public string TargetAlias;
253+
[MarshalAs(UnmanagedType.LPWStr)] public string TargetAlias;
257254

258-
[MarshalAs(UnmanagedType.LPWStr)]
259-
public string UserName;
255+
[MarshalAs(UnmanagedType.LPWStr)] public string UserName;
260256
}
261257
}
262258
}

App/ViewModels/SignInViewModel.cs

+2-4
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,9 @@ public partial class SignInViewModel : ObservableObject
3333
[NotifyPropertyChangedFor(nameof(ApiTokenError))]
3434
public partial bool ApiTokenTouched { get; set; } = false;
3535

36-
[ObservableProperty]
37-
public partial string? SignInError { get; set; } = null;
36+
[ObservableProperty] public partial string? SignInError { get; set; } = null;
3837

39-
[ObservableProperty]
40-
public partial bool SignInLoading { get; set; } = false;
38+
[ObservableProperty] public partial bool SignInLoading { get; set; } = false;
4139

4240
public string? CoderUrlError => CoderUrlTouched ? _coderUrlError : null;
4341

App/ViewModels/TrayWindowDisconnectedViewModel.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ public partial class TrayWindowDisconnectedViewModel : ObservableObject
1010
{
1111
private readonly IRpcController _rpcController;
1212

13-
[ObservableProperty]
14-
public partial bool ReconnectButtonEnabled { get; set; } = true;
13+
[ObservableProperty] public partial bool ReconnectButtonEnabled { get; set; } = true;
1514

1615
public TrayWindowDisconnectedViewModel(IRpcController rpcController)
1716
{

App/ViewModels/TrayWindowViewModel.cs

+4-8
Original file line numberDiff line numberDiff line change
@@ -23,15 +23,12 @@ public partial class TrayWindowViewModel : ObservableObject
2323

2424
private DispatcherQueue? _dispatcherQueue;
2525

26-
[ObservableProperty]
27-
public partial VpnLifecycle VpnLifecycle { get; set; } = VpnLifecycle.Unknown;
26+
[ObservableProperty] public partial VpnLifecycle VpnLifecycle { get; set; } = VpnLifecycle.Unknown;
2827

2928
// This is a separate property because we need the switch to be 2-way.
30-
[ObservableProperty]
31-
public partial bool VpnSwitchActive { get; set; } = false;
29+
[ObservableProperty] public partial bool VpnSwitchActive { get; set; } = false;
3230

33-
[ObservableProperty]
34-
public partial string? VpnFailedMessage { get; set; } = null;
31+
[ObservableProperty] public partial string? VpnFailedMessage { get; set; } = null;
3532

3633
[ObservableProperty]
3734
[NotifyPropertyChangedFor(nameof(NoAgents))]
@@ -49,8 +46,7 @@ public partial class TrayWindowViewModel : ObservableObject
4946

5047
public IEnumerable<AgentViewModel> VisibleAgents => ShowAllAgents ? Agents : Agents.Take(MaxAgents);
5148

52-
[ObservableProperty]
53-
public partial string DashboardUrl { get; set; } = "https://coder.com";
49+
[ObservableProperty] public partial string DashboardUrl { get; set; } = "https://coder.com";
5450

5551
public TrayWindowViewModel(IRpcController rpcController, ICredentialManager credentialManager)
5652
{

App/coder.ico

422 KB
Binary file not shown.

App/packages.lock.json

+5-5
Original file line numberDiff line numberDiff line change
@@ -112,17 +112,17 @@
112112
"System.Collections.Immutable": "9.0.0"
113113
}
114114
},
115-
"codersdk": {
115+
"Coder.Desktop.CoderSdk": {
116116
"type": "Project"
117117
},
118-
"vpn": {
118+
"Coder.Desktop.Vpn": {
119119
"type": "Project",
120120
"dependencies": {
121-
"System.IO.Pipelines": "[9.0.1, )",
122-
"Vpn.Proto": "[1.0.0, )"
121+
"Coder.Desktop.Vpn.Proto": "[1.0.0, )",
122+
"System.IO.Pipelines": "[9.0.1, )"
123123
}
124124
},
125-
"vpn.proto": {
125+
"Coder.Desktop.Vpn.Proto": {
126126
"type": "Project",
127127
"dependencies": {
128128
"Google.Protobuf": "[3.29.3, )"

Coder.Desktop.sln

+18
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "App", "App\App.csproj", "{8
2121
EndProject
2222
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Vpn.DebugClient", "Vpn.DebugClient\Vpn.DebugClient.csproj", "{1BBFDF88-B25F-4C07-99C2-30DA384DB730}"
2323
EndProject
24+
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Installer", "Installer\Installer.csproj", "{39F5B55A-09D8-477D-A3FA-ADAC29C52605}"
25+
EndProject
2426
Global
2527
GlobalSection(SolutionConfigurationPlatforms) = preSolution
2628
Debug|Any CPU = Debug|Any CPU
@@ -185,6 +187,22 @@ Global
185187
{1BBFDF88-B25F-4C07-99C2-30DA384DB730}.Release|x64.Build.0 = Release|Any CPU
186188
{1BBFDF88-B25F-4C07-99C2-30DA384DB730}.Release|x86.ActiveCfg = Release|Any CPU
187189
{1BBFDF88-B25F-4C07-99C2-30DA384DB730}.Release|x86.Build.0 = Release|Any CPU
190+
{39F5B55A-09D8-477D-A3FA-ADAC29C52605}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
191+
{39F5B55A-09D8-477D-A3FA-ADAC29C52605}.Debug|Any CPU.Build.0 = Debug|Any CPU
192+
{39F5B55A-09D8-477D-A3FA-ADAC29C52605}.Debug|ARM64.ActiveCfg = Debug|Any CPU
193+
{39F5B55A-09D8-477D-A3FA-ADAC29C52605}.Debug|ARM64.Build.0 = Debug|Any CPU
194+
{39F5B55A-09D8-477D-A3FA-ADAC29C52605}.Debug|x64.ActiveCfg = Debug|Any CPU
195+
{39F5B55A-09D8-477D-A3FA-ADAC29C52605}.Debug|x64.Build.0 = Debug|Any CPU
196+
{39F5B55A-09D8-477D-A3FA-ADAC29C52605}.Debug|x86.ActiveCfg = Debug|Any CPU
197+
{39F5B55A-09D8-477D-A3FA-ADAC29C52605}.Debug|x86.Build.0 = Debug|Any CPU
198+
{39F5B55A-09D8-477D-A3FA-ADAC29C52605}.Release|Any CPU.ActiveCfg = Release|Any CPU
199+
{39F5B55A-09D8-477D-A3FA-ADAC29C52605}.Release|Any CPU.Build.0 = Release|Any CPU
200+
{39F5B55A-09D8-477D-A3FA-ADAC29C52605}.Release|ARM64.ActiveCfg = Release|Any CPU
201+
{39F5B55A-09D8-477D-A3FA-ADAC29C52605}.Release|ARM64.Build.0 = Release|Any CPU
202+
{39F5B55A-09D8-477D-A3FA-ADAC29C52605}.Release|x64.ActiveCfg = Release|Any CPU
203+
{39F5B55A-09D8-477D-A3FA-ADAC29C52605}.Release|x64.Build.0 = Release|Any CPU
204+
{39F5B55A-09D8-477D-A3FA-ADAC29C52605}.Release|x86.ActiveCfg = Release|Any CPU
205+
{39F5B55A-09D8-477D-A3FA-ADAC29C52605}.Release|x86.Build.0 = Release|Any CPU
188206
EndGlobalSection
189207
GlobalSection(SolutionProperties) = preSolution
190208
HideSolutionNode = FALSE

CoderSdk/CoderSdk.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3+
<AssemblyName>Coder.Desktop.CoderSdk</AssemblyName>
34
<RootNamespace>Coder.Desktop.CoderSdk</RootNamespace>
45
<TargetFramework>net8.0</TargetFramework>
56
<ImplicitUsings>enable</ImplicitUsings>

Installer/Installer.csproj

+24
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<Project Sdk="Microsoft.NET.Sdk">
2+
3+
<PropertyGroup>
4+
<AssemblyName>Coder.Desktop.Installer</AssemblyName>
5+
<RootNamespace>Coder.Desktop.Installer</RootNamespace>
6+
<OutputType>Exe</OutputType>
7+
<TargetFramework>net481</TargetFramework>
8+
<LangVersion>13.0</LangVersion>
9+
</PropertyGroup>
10+
11+
<ItemGroup>
12+
<None Remove="*.msi" />
13+
<None Remove="*.exe" />
14+
<None Remove="*.wxs" />
15+
<None Remove="*.wixpdb" />
16+
<None Remove="*.wixobj" />
17+
</ItemGroup>
18+
19+
<ItemGroup>
20+
<PackageReference Include="WixSharp_wix4" Version="2.6.0" />
21+
<PackageReference Include="WixSharp_wix4.bin" Version="2.6.0" />
22+
<PackageReference Include="CommandLineParser" Version="2.9.1" />
23+
</ItemGroup>
24+
</Project>

0 commit comments

Comments
 (0)