diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index dae0b3d3b392e..a209df4343592 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -65,13 +65,14 @@ jobs: restore-keys: | js-${{ runner.os }}- + - name: Install nsis and zstd + run: sudo apt-get install -y nsis zstd + - name: Install nfpm run: | set -euo pipefail wget -O /tmp/nfpm.deb https://github.com/goreleaser/nfpm/releases/download/v2.18.1/nfpm_amd64.deb sudo dpkg -i /tmp/nfpm.deb - - name: Install zstd - run: sudo apt-get install -y zstd - name: Install rcodesign run: | @@ -107,6 +108,7 @@ jobs: make -j \ build/coder_"$version"_linux_{amd64,armv7,arm64}.{tar.gz,apk,deb,rpm} \ build/coder_"$version"_{darwin,windows}_{amd64,arm64}.zip \ + build/coder_"$version"_windows_amd64_installer.exe \ build/coder_helm_"$version".tgz env: CODER_SIGN_DARWIN: "1" @@ -155,6 +157,7 @@ jobs: run: | ./scripts/publish_release.sh \ ${{ (github.event.inputs.dry_run || github.event.inputs.snapshot) && '--dry-run' }} \ + ./build/*_installer.exe \ ./build/*.zip \ ./build/*.tar.gz \ ./build/*.tgz \ diff --git a/Makefile b/Makefile index 6accef31c5432..64995b7a2168a 100644 --- a/Makefile +++ b/Makefile @@ -252,6 +252,13 @@ $(CODER_ALL_PACKAGES): $(CODER_PACKAGE_DEPS) --output "$@" \ "build/coder_$(VERSION)_$${os}_$${arch}" +# This task builds a Windows amd64 installer. Depends on makensis. +build/coder_$(VERSION)_windows_amd64_installer.exe: build/coder_$(VERSION)_windows_amd64.exe + ./scripts/build_windows_installer.sh \ + --version "$(VERSION)" \ + --output "$@" \ + "$<" + # Redirect from version-less Docker image targets to the versioned ones. # # Called like this: diff --git a/README.md b/README.md index ccc01b3fc908c..336d2dec00b4b 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,10 @@ Coder creates remote development machines so your team can develop from anywhere > **Note**: > Coder is in a beta state. [Report issues here](https://github.com/coder/coder/issues/new). -The easiest way to install Coder is to use our [install script](https://github.com/coder/coder/blob/main/install.sh) for Linux and macOS. +The easiest way to install Coder is to use our +[install script](https://github.com/coder/coder/blob/main/install.sh) for Linux +and macOS. For Windows, use the latest `..._installer.exe` file from GitHub +Releases. To install, run: diff --git a/scripts/build_windows_installer.sh b/scripts/build_windows_installer.sh new file mode 100755 index 0000000000000..f7ddbd4b3653e --- /dev/null +++ b/scripts/build_windows_installer.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash + +# This script packages a Windows installer for Coder containing the given +# binary. +# +# Usage: ./build_windows_installer.sh --output "path/to/installer.exe" [--version 1.2.3] [--agpl] path/to/binary.exe +# +# Only amd64 binaries are supported. +# +# If no version is specified, defaults to the version from ./version.sh. +# +# If the --agpl parameter is specified, only the AGPL license is included in the +# installer. + +set -euo pipefail +# shellcheck source=scripts/lib.sh +source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" + +agpl="${CODER_BUILD_AGPL:-0}" +output_path="" +version="" + +args="$(getopt -o "" -l agpl,output:,version: -- "$@")" +eval set -- "$args" +while true; do + case "$1" in + --agpl) + agpl=1 + shift + ;; + --output) + mkdir -p "$(dirname "$2")" + output_path="$(realpath "$2")" + shift 2 + ;; + --version) + version="$2" + shift 2 + ;; + --) + shift + break + ;; + *) + error "Unrecognized option: $1" + ;; + esac +done + +if [[ "$output_path" == "" ]]; then + error "--output is a required parameter" +fi + +if [[ "$#" != 1 ]]; then + error "Exactly one argument must be provided to this script, $# were supplied" +fi +if [[ ! -f "$1" ]]; then + error "File '$1' does not exist or is not a regular file" +fi +input_file="$(realpath "$1")" + +version="${version#v}" +if [[ "$version" == "" ]]; then + version="$(execrelative ./version.sh)" +fi + +# Remove the "v" prefix and ensure the version is in the format X.X.X.X for +# makensis. +nsis_version="${version//-*/}" +nsis_version+=".$(date -u +%Y%m%d%H%M)" + +# Check dependencies +dependencies makensis + +# Make a temporary dir where all source files intended to be in the installer +# will be hardlinked/copied to. +cdroot +temp_dir="$(TMPDIR="$(dirname "$input_file")" mktemp -d)" +mkdir -p "$temp_dir/bin" +ln "$input_file" "$temp_dir/bin/coder.exe" +cp "$(realpath scripts/win-installer/installer.nsi)" "$temp_dir/installer.nsi" +cp "$(realpath scripts/win-installer/path.nsh)" "$temp_dir/path.nsh" +cp "$(realpath scripts/win-installer/coder.ico)" "$temp_dir/coder.ico" +cp "$(realpath scripts/win-installer/banner.bmp)" "$temp_dir/banner.bmp" + +# Craft a license document by combining the AGPL license and optionally the +# enterprise license. +license_path="$temp_dir/license.txt" + +if [[ "$agpl" == 0 ]]; then + cat <<-EOF >"$license_path" + This distribution of Coder includes some enterprise-licensed code which is not + licensed under the AGPL license: + + $(sed 's/^/ /' "$(realpath LICENSE.enterprise)") + + + + The non-enterprise code in this distribution is licensed under the AGPL license: + + $(sed 's/^/ /' "$(realpath LICENSE)") + EOF +else + cat <<-EOF >"$license_path" + This distribution of Coder is free software and is licensed under the AGPL + license: + + $(sed 's/^/ /' "$(realpath LICENSE)") + EOF +fi + +# Run makensis to build the installer. +pushd "$temp_dir" +makensis \ + -V4 \ + -DCODER_VERSION="$version" \ + -DCODER_NSIS_VERSION="$nsis_version" \ + -DCODER_YEAR="$(date +%Y)" \ + installer.nsi +popd + +# Copy the installer to the output path. +cp "$temp_dir/installer.exe" "$output_path" + +rm -rf "$temp_dir" diff --git a/scripts/package.sh b/scripts/package.sh index 50db188aba9bd..dcd5614ae145a 100755 --- a/scripts/package.sh +++ b/scripts/package.sh @@ -79,7 +79,7 @@ if [[ "$arch" == "arm" ]] || [[ "$arch" == "armv7" ]]; then fi # Make temporary dir where all source files intended to be in the package will -# be hardlinked from. +# be hardlinked to. cdroot temp_dir="$(TMPDIR="$(dirname "$input_file")" mktemp -d)" ln "$input_file" "$temp_dir/coder" diff --git a/scripts/win-installer/banner.bmp b/scripts/win-installer/banner.bmp new file mode 100644 index 0000000000000..2d3ec844a8599 Binary files /dev/null and b/scripts/win-installer/banner.bmp differ diff --git a/scripts/win-installer/coder.ico b/scripts/win-installer/coder.ico new file mode 100644 index 0000000000000..e13ad3bd4cb3a Binary files /dev/null and b/scripts/win-installer/coder.ico differ diff --git a/scripts/win-installer/installer.nsi b/scripts/win-installer/installer.nsi new file mode 100644 index 0000000000000..ae9c5593f2b8b --- /dev/null +++ b/scripts/win-installer/installer.nsi @@ -0,0 +1,133 @@ +# This NSIS installer script was taken from the following webpage and heavily +# adapted to Coder's needs: +# https://www.conjur.org/blog/building-a-windows-installer-from-a-linux-ci-pipeline/ + +Unicode true + +!define APP_NAME "Coder" +!define COMP_NAME "Coder Technologies, Inc." +!define VERSION "${CODER_NSIS_VERSION}" +!define COPYRIGHT "Copyright (c) ${CODER_YEAR} Coder Technologies, Inc." +!define DESCRIPTION "Remote development environments on your infrastructure provisioned with Terraform" +!define INSTALLER_NAME "installer.exe" +!define MAIN_APP_EXE "coder.exe" +!define MAIN_APP_EXE_PATH "bin\coder.exe" +!define ICON "coder.ico" +!define BANNER "banner.bmp" +!define LICENSE_TXT "license.txt" + +!define INSTALL_DIR "$PROGRAMFILES64\${APP_NAME}" +!define INSTALL_TYPE "SetShellVarContext all" # this means install for all users +!define REG_ROOT "HKLM" +!define REG_APP_PATH "Software\Microsoft\Windows\CurrentVersion\App Paths\${MAIN_APP_EXE}" +!define UNINSTALL_PATH "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_NAME}" + +###################################################################### + +VIProductVersion "${VERSION}" +VIAddVersionKey "ProductName" "${APP_NAME}" +VIAddVersionKey "CompanyName" "${COMP_NAME}" +VIAddVersionKey "LegalCopyright" "${COPYRIGHT}" +VIAddVersionKey "FileDescription" "${DESCRIPTION}" +VIAddVersionKey "FileVersion" "${VERSION}" + +###################################################################### + +SetCompressor /SOLID Lzma +Name "${APP_NAME}" +Caption "${APP_NAME}" +OutFile "${INSTALLER_NAME}" +BrandingText "${APP_NAME} v${CODER_VERSION}" +InstallDirRegKey "${REG_ROOT}" "${REG_APP_PATH}" "Path" +InstallDir "${INSTALL_DIR}" + +###################################################################### + +!define MUI_ICON "${ICON}" +!define MUI_UNICON "${ICON}" +!define MUI_WELCOMEFINISHPAGE_BITMAP "${BANNER}" +!define MUI_UNWELCOMEFINISHPAGE_BITMAP "${BANNER}" + +###################################################################### + +!include "MUI2.nsh" + +!define MUI_ABORTWARNING +!define MUI_UNABORTWARNING + +!define MUI_WELCOMEPAGE_TEXT "Setup will guide you through the installation of Coder v${CODER_VERSION}.$\r$\n$\r$\nClick Next to continue." + +!insertmacro MUI_PAGE_WELCOME + +!insertmacro MUI_PAGE_LICENSE "${LICENSE_TXT}" + +!insertmacro MUI_PAGE_COMPONENTS + +!insertmacro MUI_PAGE_DIRECTORY + +!insertmacro MUI_PAGE_INSTFILES + +!define MUI_FINISHPAGE_TEXT "Coder v${CODER_VERSION} has been installed on your computer.$\r$\n$\r$\nIf you added Coder to your PATH, you can use Coder by opening a command prompt or PowerShell and running `coder`. You may have to sign out and sign back in for `coder` to be available.$\r$\n$\r$\nClick Finish to close Setup." + +!insertmacro MUI_PAGE_FINISH + +!insertmacro MUI_UNPAGE_CONFIRM + +!insertmacro MUI_UNPAGE_INSTFILES + +!insertmacro MUI_UNPAGE_FINISH + +!insertmacro MUI_LANGUAGE "English" + +###################################################################### + +!include ".\path.nsh" + +Section "Coder CLI" SecInstall + SectionIn RO # mark this section as required + + ${INSTALL_TYPE} + + SetOverwrite ifnewer + SetOutPath "$INSTDIR" + File /r "bin" + File "${LICENSE_TXT}" + + WriteUninstaller "$INSTDIR\uninstall.exe" + + WriteRegStr ${REG_ROOT} "${REG_APP_PATH}" "" "$INSTDIR\${MAIN_APP_EXE_PATH}" + WriteRegStr ${REG_ROOT} "${REG_APP_PATH}" "Path" "$INSTDIR" + WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayName" "${APP_NAME}" + WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "UninstallString" "$INSTDIR\uninstall.exe" + WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayIcon" "$INSTDIR\${MAIN_APP_EXE_PATH}" + WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "DisplayVersion" "${VERSION}" + WriteRegStr ${REG_ROOT} "${UNINSTALL_PATH}" "Publisher" "${COMP_NAME}" +SectionEnd + +Section "Add to PATH" SecAddToPath + Push "$INSTDIR\bin" + Call AddToPath +SectionEnd + +###################################################################### + +Section Uninstall + ${INSTALL_TYPE} + + RmDir /r "$INSTDIR" + DeleteRegKey ${REG_ROOT} "${REG_APP_PATH}" + DeleteRegKey ${REG_ROOT} "${UNINSTALL_PATH}" + + Push "$INSTDIR\bin" + Call un.RemoveFromPath +SectionEnd + +###################################################################### + +LangString DESC_SecInstall ${LANG_ENGLISH} "Install the Coder command-line interface (coder.exe) for all users." +LangString DESC_SecAddToPath ${LANG_ENGLISH} "Add coder.exe to the PATH for all users. This enables `coder` to be used directly from a command prompt or PowerShell." + +!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN + !insertmacro MUI_DESCRIPTION_TEXT ${SecInstall} $(DESC_SecInstall) + !insertmacro MUI_DESCRIPTION_TEXT ${SecAddToPath} $(DESC_SecAddToPath) +!insertmacro MUI_FUNCTION_DESCRIPTION_END diff --git a/scripts/win-installer/path.nsh b/scripts/win-installer/path.nsh new file mode 100644 index 0000000000000..d818e1ec5a12e --- /dev/null +++ b/scripts/win-installer/path.nsh @@ -0,0 +1,198 @@ +# PATH utilities. Taken from: +# https://www.smartmontools.org/browser/trunk/smartmontools/os_win32/installer.nsi?rev=5310#L689 + +; os_win32/installer.nsi - smartmontools install NSIS script +; +; Home page of code is: https://www.smartmontools.org +; +; Copyright (C) 2006-22 Christian Franke +; +; SPDX-License-Identifier: GPL-2.0-or-later + + +;-------------------------------------------------------------------- +; Path functions +; +; Based on example from: +; http://nsis.sourceforge.net/Path_Manipulation +; + + +!include "WinMessages.nsh" + +; Registry Entry for environment (NT4,2000,XP) +; All users: +!define Environ 'HKLM "SYSTEM\CurrentControlSet\Control\Session Manager\Environment"' +; Current user only: +;!define Environ 'HKCU "Environment"' + + +; AddToPath - Appends dir to PATH +; (does not work on Win9x/ME) +; +; Usage: +; Push "dir" +; Call AddToPath + +Function AddToPath + Exch $0 + Push $1 + Push $2 + Push $3 + Push $4 + + ; NSIS ReadRegStr returns empty string on string overflow + ; Native calls are used here to check actual length of PATH + + ; $4 = RegOpenKey(HKEY_CURRENT_USER, "Environment", &$3) + System::Call "advapi32::RegOpenKey(i 0x80000001, t'Environment', *i.r3) i.r4" + IntCmp $4 0 0 done done + ; $4 = RegQueryValueEx($3, "PATH", (DWORD*)0, (DWORD*)0, &$1, ($2=NSIS_MAX_STRLEN, &$2)) + ; RegCloseKey($3) + System::Call "advapi32::RegQueryValueEx(i $3, t'PATH', i 0, i 0, t.r1, *i ${NSIS_MAX_STRLEN} r2) i.r4" + System::Call "advapi32::RegCloseKey(i $3)" + + ${If} $4 = 234 ; ERROR_MORE_DATA + DetailPrint "AddToPath: original length $2 > ${NSIS_MAX_STRLEN}" + MessageBox MB_OK "PATH not updated, original length $2 > ${NSIS_MAX_STRLEN}" /SD IDOK + Goto done + ${EndIf} + + ${If} $4 <> 0 ; NO_ERROR + ${If} $4 <> 2 ; ERROR_FILE_NOT_FOUND + DetailPrint "AddToPath: unexpected error code $4" + Goto done + ${EndIf} + StrCpy $1 "" + ${EndIf} + + ; Check if already in PATH + Push "$1;" + Push "$0;" + Call StrStr + Pop $2 + StrCmp $2 "" 0 done + Push "$1;" + Push "$0\;" + Call StrStr + Pop $2 + StrCmp $2 "" 0 done + + ; Prevent NSIS string overflow + StrLen $2 $0 + StrLen $3 $1 + IntOp $2 $2 + $3 + IntOp $2 $2 + 2 ; $2 = strlen(dir) + strlen(PATH) + sizeof(";") + ${If} $2 > ${NSIS_MAX_STRLEN} + DetailPrint "AddToPath: new length $2 > ${NSIS_MAX_STRLEN}" + MessageBox MB_OK "PATH not updated, new length $2 > ${NSIS_MAX_STRLEN}." /SD IDOK + Goto done + ${EndIf} + + ; Append dir to PATH + DetailPrint "Add to PATH: $0" + StrCpy $2 $1 1 -1 + ${If} $2 == ";" + StrCpy $1 $1 -1 ; remove trailing ';' + ${EndIf} + ${If} $1 != "" ; no leading ';' + StrCpy $0 "$1;$0" + ${EndIf} + WriteRegExpandStr ${Environ} "PATH" $0 + SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000 + +done: + Pop $4 + Pop $3 + Pop $2 + Pop $1 + Pop $0 +FunctionEnd + + +; RemoveFromPath - Removes dir from PATH +; +; Usage: +; Push "dir" +; Call RemoveFromPath + +Function un.RemoveFromPath + Exch $0 + Push $1 + Push $2 + Push $3 + Push $4 + Push $5 + Push $6 + + ReadRegStr $1 ${Environ} "PATH" + StrCpy $5 $1 1 -1 + ${If} $5 != ";" + StrCpy $1 "$1;" ; ensure trailing ';' + ${EndIf} + Push $1 + Push "$0;" + Call un.StrStr + Pop $2 ; pos of our dir + StrCmp $2 "" done + + DetailPrint "Remove from PATH: $0" + StrLen $3 "$0;" + StrLen $4 $2 + StrCpy $5 $1 -$4 ; $5 is now the part before the path to remove + StrCpy $6 $2 "" $3 ; $6 is now the part after the path to remove + StrCpy $3 "$5$6" + StrCpy $5 $3 1 -1 + ${If} $5 == ";" + StrCpy $3 $3 -1 ; remove trailing ';' + ${EndIf} + WriteRegExpandStr ${Environ} "PATH" $3 + SendMessage ${HWND_BROADCAST} ${WM_WININICHANGE} 0 "STR:Environment" /TIMEOUT=5000 + +done: + Pop $6 + Pop $5 + Pop $4 + Pop $3 + Pop $2 + Pop $1 + Pop $0 +FunctionEnd + + +; StrStr - find substring in a string +; +; Usage: +; Push "this is some string" +; Push "some" +; Call StrStr +; Pop $0 ; "some string" + +!macro StrStr un +Function ${un}StrStr + Exch $R1 ; $R1=substring, stack=[old$R1,string,...] + Exch ; stack=[string,old$R1,...] + Exch $R2 ; $R2=string, stack=[old$R2,old$R1,...] + Push $R3 + Push $R4 + Push $R5 + StrLen $R3 $R1 + StrCpy $R4 0 + ; $R1=substring, $R2=string, $R3=strlen(substring) + ; $R4=count, $R5=tmp + ${Do} + StrCpy $R5 $R2 $R3 $R4 + ${IfThen} $R5 == $R1 ${|} ${ExitDo} ${|} + ${IfThen} $R5 == "" ${|} ${ExitDo} ${|} + IntOp $R4 $R4 + 1 + ${Loop} + StrCpy $R1 $R2 "" $R4 + Pop $R5 + Pop $R4 + Pop $R3 + Pop $R2 + Exch $R1 ; $R1=old$R1, stack=[result,...] +FunctionEnd +!macroend +!insertmacro StrStr "" +!insertmacro StrStr "un."