From 70cdde53300f78f2cec4c94e0b77be073d66e263 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Thu, 14 Nov 2024 11:46:27 +0000 Subject: [PATCH 1/2] chore: support building Coder Desktop `.dylib` --- .github/workflows/ci.yaml | 95 +++++++++++++++++++++++++++++++++- .github/workflows/release.yaml | 93 +++++++++++++++++++++++++++++++-- Makefile | 23 ++++++++ scripts/build_go.sh | 30 +++++++++-- scripts/release/publish.sh | 11 ++-- scripts/sign_darwin.sh | 17 ++++-- vpn/dylib/lib.go | 53 +++++++++++++++++++ 7 files changed, 306 insertions(+), 16 deletions(-) create mode 100644 vpn/dylib/lib.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 045c6d497a4e6..6112905d8be00 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -806,10 +806,91 @@ jobs: echo "Required checks have passed" + # Builds the dylibs and upload it as an artifact so it can be embedded in the main build + build-dylib: + needs: changes + # We always build the dylibs on Go changes to verify we're not merging unbuildable code, + # but they need only be signed and uploaded on coder/coder main. + if: needs.changes.outputs.docs-only == 'false' || github.ref == 'refs/heads/main' + runs-on: ${{ github.repository_owner == 'coder' && 'depot-macos-latest' || 'macos-latest' }} + steps: + - name: Harden Runner + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + with: + fetch-depth: 0 + + - name: Setup build tools + run: | + brew install bash gnu-getopt make + echo "$(brew --prefix bash)/bin" >> $GITHUB_PATH + echo "$(brew --prefix gnu-getopt)/bin" >> $GITHUB_PATH + echo "$(brew --prefix make)/libexec/gnubin" >> $GITHUB_PATH + + - name: Setup Go + uses: ./.github/actions/setup-go + + - name: Install rcodesign + if: ${{ github.repository_owner == 'coder' && github.ref == 'refs/heads/main' }} + run: | + set -euo pipefail + wget -O /tmp/rcodesign.tar.gz https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz + sudo tar -xzf /tmp/rcodesign.tar.gz \ + -C /usr/local/bin \ + --strip-components=1 \ + apple-codesign-0.22.0-macos-universal/rcodesign + rm /tmp/rcodesign.tar.gz + + - name: Setup Apple Developer certificate and API key + if: ${{ github.repository_owner == 'coder' && github.ref == 'refs/heads/main' }} + run: | + set -euo pipefail + touch /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8} + chmod 600 /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8} + echo "$AC_CERTIFICATE_P12_BASE64" | base64 -d > /tmp/apple_cert.p12 + echo "$AC_CERTIFICATE_PASSWORD" > /tmp/apple_cert_password.txt + echo "$AC_APIKEY_P8_BASE64" | base64 -d > /tmp/apple_apikey.p8 + env: + AC_CERTIFICATE_P12_BASE64: ${{ secrets.AC_CERTIFICATE_P12_BASE64 }} + AC_CERTIFICATE_PASSWORD: ${{ secrets.AC_CERTIFICATE_PASSWORD }} + AC_APIKEY_P8_BASE64: ${{ secrets.AC_APIKEY_P8_BASE64 }} + + - name: Build dylibs + run: | + set -euxo pipefail + go mod download + + make gen/mark-fresh + make build/coder-dylib + env: + CODER_SIGN_DARWIN: ${{ github.ref == 'refs/heads/main' && '1' || '0' }} + AC_CERTIFICATE_FILE: /tmp/apple_cert.p12 + AC_CERTIFICATE_PASSWORD_FILE: /tmp/apple_cert_password.txt + + - name: Upload build artifacts + if: ${{ github.repository_owner == 'coder' && github.ref == 'refs/heads/main' }} + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: dylibs + path: | + ./build/*.h + ./build/*.dylib + retention-days: 7 + + - name: Delete Apple Developer certificate and API key + if: ${{ github.repository_owner == 'coder' && github.ref == 'refs/heads/main' }} + run: rm -f /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8} + build: # This builds and publishes ghcr.io/coder/coder-preview:main for each commit # to main branch. - needs: changes + needs: + - changes + - build-dylib if: github.ref == 'refs/heads/main' && needs.changes.outputs.docs-only == 'false' && !github.event.pull_request.head.repo.fork runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }} permissions: @@ -848,6 +929,18 @@ jobs: - name: Install zstd run: sudo apt-get install -y zstd + - name: Download dylibs + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: dylibs + path: ./build + + - name: Insert dylibs + run: | + mv ./build/*amd64.dylib ./site/out/bin/coder-vpn-darwin-amd64.dylib + mv ./build/*arm64.dylib ./site/out/bin/coder-vpn-darwin-arm64.dylib + mv ./build/*arm64.h ./site/out/bin/coder-vpn-darwin-dylib.h + - name: Build run: | set -euxo pipefail diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 92898ebe5452d..4e4971a382746 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -32,8 +32,80 @@ env: CODER_RELEASE_NOTES: ${{ inputs.release_notes }} jobs: + # build-dylib is a separate job to build the dylib on macOS. + build-dylib: + runs-on: ${{ github.repository_owner == 'coder' && 'depot-macos-latest' || 'macos-latest' }} + steps: + - name: Harden Runner + uses: step-security/harden-runner@91182cccc01eb5e619899d80e4e971d6181294a7 # v2.10.1 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 + with: + fetch-depth: 0 + + - name: Setup build tools + run: | + brew install bash gnu-getopt make + echo "$(brew --prefix bash)/bin" >> $GITHUB_PATH + echo "$(brew --prefix gnu-getopt)/bin" >> $GITHUB_PATH + echo "$(brew --prefix make)/libexec/gnubin" >> $GITHUB_PATH + + - name: Setup Go + uses: ./.github/actions/setup-go + + - name: Install rcodesign + run: | + set -euo pipefail + wget -O /tmp/rcodesign.tar.gz https://github.com/indygreg/apple-platform-rs/releases/download/apple-codesign%2F0.22.0/apple-codesign-0.22.0-macos-universal.tar.gz + sudo tar -xzf /tmp/rcodesign.tar.gz \ + -C /usr/local/bin \ + --strip-components=1 \ + apple-codesign-0.22.0-macos-universal/rcodesign + rm /tmp/rcodesign.tar.gz + + - name: Setup Apple Developer certificate and API key + run: | + set -euo pipefail + touch /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8} + chmod 600 /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8} + echo "$AC_CERTIFICATE_P12_BASE64" | base64 -d > /tmp/apple_cert.p12 + echo "$AC_CERTIFICATE_PASSWORD" > /tmp/apple_cert_password.txt + echo "$AC_APIKEY_P8_BASE64" | base64 -d > /tmp/apple_apikey.p8 + env: + AC_CERTIFICATE_P12_BASE64: ${{ secrets.AC_CERTIFICATE_P12_BASE64 }} + AC_CERTIFICATE_PASSWORD: ${{ secrets.AC_CERTIFICATE_PASSWORD }} + AC_APIKEY_P8_BASE64: ${{ secrets.AC_APIKEY_P8_BASE64 }} + + - name: Build dylibs + run: | + set -euxo pipefail + go mod download + + make gen/mark-fresh + make build/coder-dylib + env: + CODER_SIGN_DARWIN: 1 + AC_CERTIFICATE_FILE: /tmp/apple_cert.p12 + AC_CERTIFICATE_PASSWORD_FILE: /tmp/apple_cert_password.txt + + - name: Upload build artifacts + uses: actions/upload-artifact@b4b15b8c7c6ac21ea08fcf65892d2ee8f75cf882 # v4.4.3 + with: + name: dylibs + path: | + ./build/*.h + ./build/*.dylib + retention-days: 7 + + - name: Delete Apple Developer certificate and API key + run: rm -f /tmp/{apple_cert.p12,apple_cert_password.txt,apple_apikey.p8} + release: name: Build and publish + needs: build-dylib runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }} permissions: # Required to publish a release @@ -145,6 +217,18 @@ jobs: - name: Install nsis and zstd run: sudo apt-get install -y nsis zstd + - name: Download dylibs + uses: actions/download-artifact@fa0a91b85d4f404e444e00e005971372dc801d16 # v4.1.8 + with: + name: dylibs + path: ./build + + - name: Insert dylibs + run: | + mv ./build/*amd64.dylib ./site/out/bin/coder-vpn-darwin-amd64.dylib + mv ./build/*arm64.dylib ./site/out/bin/coder-vpn-darwin-arm64.dylib + mv ./build/*arm64.h ./site/out/bin/coder-vpn-darwin-dylib.h + - name: Install nfpm run: | set -euo pipefail @@ -271,6 +355,7 @@ jobs: ${{ steps.image-base-tag.outputs.tag }} - name: Verify that images are pushed properly + if: steps.image-base-tag.outputs.tag != '' run: | # retry 10 times with a 5 second delay as the images may not be # available immediately @@ -303,10 +388,6 @@ jobs: run: | set -euxo pipefail - # build Docker images for each architecture - version="$(./scripts/version.sh)" - make build/coder_"$version"_linux_{amd64,arm64,armv7}.tag - # we can't build multi-arch if the images aren't pushed, so quit now # if dry-running if [[ "$CODER_RELEASE" != *t* ]]; then @@ -314,6 +395,10 @@ jobs: exit 0 fi + # build Docker images for each architecture + version="$(./scripts/version.sh)" + make build/coder_"$version"_linux_{amd64,arm64,armv7}.tag + # build and push multi-arch manifest, this depends on the other images # being pushed so will automatically push them. make push/build/coder_"$version"_linux.tag diff --git a/Makefile b/Makefile index 88664710a067b..7a91b70d768bb 100644 --- a/Makefile +++ b/Makefile @@ -79,8 +79,12 @@ PACKAGE_OS_ARCHES := linux_amd64 linux_armv7 linux_arm64 # All architectures we build Docker images for (Linux only). DOCKER_ARCHES := amd64 arm64 armv7 +# All ${OS}_${ARCH} combos we build the desktop dylib for. +DYLIB_ARCHES := darwin_amd64 darwin_arm64 + # Computed variables based on the above. CODER_SLIM_BINARIES := $(addprefix build/coder-slim_$(VERSION)_,$(OS_ARCHES)) +CODER_DYLIBS := $(foreach os_arch, $(DYLIB_ARCHES), build/coder-vpn_$(VERSION)_$(os_arch).dylib) CODER_FAT_BINARIES := $(addprefix build/coder_$(VERSION)_,$(OS_ARCHES)) CODER_ALL_BINARIES := $(CODER_SLIM_BINARIES) $(CODER_FAT_BINARIES) CODER_TAR_GZ_ARCHIVES := $(foreach os_arch, $(ARCHIVE_TAR_GZ), build/coder_$(VERSION)_$(os_arch).tar.gz) @@ -238,6 +242,25 @@ $(CODER_ALL_BINARIES): go.mod go.sum \ cp "$@" "./site/out/bin/coder-$$os-$$arch$$dot_ext" fi +# This task builds Coder Desktop dylibs +$(CODER_DYLIBS): go.mod go.sum $(GO_SRC_FILES) + @if [ "$(shell uname)" = "Darwin" ]; then + $(get-mode-os-arch-ext) + ./scripts/build_go.sh \ + --os "$$os" \ + --arch "$$arch" \ + --version "$(VERSION)" \ + --output "$@" \ + --dylib + + else + echo "ERROR: Can't build dylib on non-Darwin OS" 1>&2 + exit 1 + fi + +# This task builds both dylibs +build/coder-dylib: $(CODER_DYLIBS) + # This task builds all archives. It parses the target name to get the metadata # for the build, so it must be specified in this format: # build/coder_${version}_${os}_${arch}.${format} diff --git a/scripts/build_go.sh b/scripts/build_go.sh index bfecb6763ac7b..8dd483937197a 100755 --- a/scripts/build_go.sh +++ b/scripts/build_go.sh @@ -2,7 +2,7 @@ # This script builds a single Go binary of Coder with the given parameters. # -# Usage: ./build_go.sh [--version 1.2.3-devel+abcdef] [--os linux] [--arch amd64] [--output path/to/output] [--slim] [--agpl] [--boringcrypto] +# Usage: ./build_go.sh [--version 1.2.3-devel+abcdef] [--os linux] [--arch amd64] [--output path/to/output] [--slim] [--agpl] [--boringcrypto] [--dylib] # # Defaults to linux:amd64 with slim disabled, but can be controlled with GOOS, # GOARCH and CODER_SLIM_BUILD=1. If no version is specified, defaults to the @@ -25,6 +25,9 @@ # # If the --boringcrypto parameter is specified, builds use boringcrypto instead of # the standard go crypto libraries. +# +# If the --dylib parameter is specified, the Coder Desktop `.dylib` is built +# instead of the standard binary. This is only supported on macOS arm64 & amd64. set -euo pipefail # shellcheck source=scripts/lib.sh @@ -36,12 +39,14 @@ arch="${GOARCH:-amd64}" slim="${CODER_SLIM_BUILD:-0}" sign_darwin="${CODER_SIGN_DARWIN:-0}" sign_windows="${CODER_SIGN_WINDOWS:-0}" +bin_ident="com.coder.cli" output_path="" agpl="${CODER_BUILD_AGPL:-0}" boringcrypto=${CODER_BUILD_BORINGCRYPTO:-0} debug=0 +dylib=0 -args="$(getopt -o "" -l version:,os:,arch:,output:,slim,agpl,sign-darwin,boringcrypto,debug -- "$@")" +args="$(getopt -o "" -l version:,os:,arch:,output:,slim,agpl,sign-darwin,boringcrypto,dylib,debug -- "$@")" eval set -- "$args" while true; do case "$1" in @@ -78,6 +83,10 @@ while true; do boringcrypto=1 shift ;; + --dylib) + dylib=1 + shift + ;; --debug) debug=1 shift @@ -168,18 +177,31 @@ if [[ "$agpl" == 1 ]]; then fi cgo=0 +if [[ "$dylib" == 1 ]]; then + if [[ "$os" != "darwin" ]]; then + error "dylib builds are not supported on $os" + fi + cgo=1 + cmd_path="./vpn/dylib/lib.go" + build_args+=("-buildmode=c-shared") + SDKROOT="$(xcrun --sdk macosx --show-sdk-path)" + export SDKROOT + bin_ident="com.coder.vpn" +fi + goexp="" if [[ "$boringcrypto" == 1 ]]; then cgo=1 goexp="boringcrypto" fi -GOEXPERIMENT="$goexp" CGO_ENABLED="$cgo" GOOS="$os" GOARCH="$arch" GOARM="$arm_version" go build \ +GOEXPERIMENT="$goexp" CGO_ENABLED="$cgo" GOOS="$os" GOARCH="$arch" GOARM="$arm_version" \ + go build \ "${build_args[@]}" \ "$cmd_path" 1>&2 if [[ "$sign_darwin" == 1 ]] && [[ "$os" == "darwin" ]]; then - execrelative ./sign_darwin.sh "$output_path" 1>&2 + execrelative ./sign_darwin.sh "$output_path" "$bin_ident" 1>&2 fi if [[ "$sign_windows" == 1 ]] && [[ "$os" == "windows" ]]; then diff --git a/scripts/release/publish.sh b/scripts/release/publish.sh index 68dbf468f40b9..df28d46ad2710 100755 --- a/scripts/release/publish.sh +++ b/scripts/release/publish.sh @@ -180,10 +180,13 @@ if [[ "$stable" == 1 ]]; then fi target_commitish=main # This is the default. -release_branch_refname=$(git branch --remotes --contains "${new_tag}" --format '%(refname)' '*/release/*') -if [[ -n "${release_branch_refname}" ]]; then - # refs/remotes/origin/release/2.9 -> release/2.9 - target_commitish="release/${release_branch_refname#*release/}" +# Skip during dry-runs +if [[ "$dry_run" == 0 ]]; then + release_branch_refname=$(git branch --remotes --contains "${new_tag}" --format '%(refname)' '*/release/*') + if [[ -n "${release_branch_refname}" ]]; then + # refs/remotes/origin/release/2.9 -> release/2.9 + target_commitish="release/${release_branch_refname#*release/}" + fi fi # We pipe `true` into `gh` so that it never tries to be interactive. diff --git a/scripts/sign_darwin.sh b/scripts/sign_darwin.sh index c1688252157e0..b1d010e5e3d8e 100755 --- a/scripts/sign_darwin.sh +++ b/scripts/sign_darwin.sh @@ -3,11 +3,14 @@ # This script signs the provided darwin binary with an Apple Developer # certificate. # -# Usage: ./sign_darwin.sh path/to/binary +# Usage: ./sign_darwin.sh path/to/binary binary_identifier # # On success, the input file will be signed using the Apple Developer # certificate. # +# For the Coder CLI, the binary_identifier should be "com.coder.cli". +# For the CoderVPN `.dylib`, the binary_identifier should be "com.coder.vpn". +# # You can check if a binary is signed by running the following command on a Mac: # codesign -dvv path/to/binary # @@ -25,15 +28,23 @@ set -euo pipefail # shellcheck source=scripts/lib.sh source "$(dirname "${BASH_SOURCE[0]}")/lib.sh" +if [[ "$#" -lt 2 ]]; then + echo "Usage: $0 path/to/binary binary_identifier" + exit 1 +fi + +BINARY_PATH="$1" +BINARY_IDENTIFIER="$2" + # Check dependencies dependencies rcodesign requiredenvs AC_CERTIFICATE_FILE AC_CERTIFICATE_PASSWORD_FILE # -v is quite verbose, the default output is pretty good on it's own. rcodesign sign \ - --binary-identifier "com.coder.cli" \ + --binary-identifier "$BINARY_IDENTIFIER" \ --p12-file "$AC_CERTIFICATE_FILE" \ --p12-password-file "$AC_CERTIFICATE_PASSWORD_FILE" \ --code-signature-flags runtime \ - "$@" \ + "$BINARY_PATH" \ 1>&2 diff --git a/vpn/dylib/lib.go b/vpn/dylib/lib.go new file mode 100644 index 0000000000000..738ab14af276f --- /dev/null +++ b/vpn/dylib/lib.go @@ -0,0 +1,53 @@ +//go:build darwin + +package main + +import "C" + +import ( + "context" + + "golang.org/x/sys/unix" + + "cdr.dev/slog" + + "github.com/coder/coder/v2/vpn" +) + +// OpenTunnel creates a new VPN tunnel by `dup`ing the provided 'PIPE' +// file descriptors for reading, writing, and logging. +// +//export OpenTunnel +func OpenTunnel(cReadFD, cWriteFD int32) int32 { + ctx := context.Background() + + readFD, err := unix.Dup(int(cReadFD)) + if err != nil { + return -1 + } + + writeFD, err := unix.Dup(int(cWriteFD)) + if err != nil { + unix.Close(readFD) + return -1 + } + + conn, err := vpn.NewBidirectionalPipe(uintptr(cReadFD), uintptr(cWriteFD)) + if err != nil { + unix.Close(readFD) + unix.Close(writeFD) + return -1 + } + + // Logs will be sent over the protocol + _, err = vpn.NewTunnel(ctx, slog.Make(), conn) + if err != nil { + unix.Close(readFD) + unix.Close(writeFD) + return -1 + } + + return 0 +} + +func main() {} From 8dc0a06301d6fa063342f35bd8e2d760b5e89642 Mon Sep 17 00:00:00 2001 From: Ethan Dickson Date: Tue, 19 Nov 2024 07:43:10 +0000 Subject: [PATCH 2/2] add err enum --- vpn/dylib/lib.go | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/vpn/dylib/lib.go b/vpn/dylib/lib.go index 738ab14af276f..346937c384e95 100644 --- a/vpn/dylib/lib.go +++ b/vpn/dylib/lib.go @@ -14,6 +14,13 @@ import ( "github.com/coder/coder/v2/vpn" ) +const ( + ErrDupReadFD = -2 + ErrDupWriteFD = -3 + ErrOpenPipe = -4 + ErrNewTunnel = -5 +) + // OpenTunnel creates a new VPN tunnel by `dup`ing the provided 'PIPE' // file descriptors for reading, writing, and logging. // @@ -23,20 +30,20 @@ func OpenTunnel(cReadFD, cWriteFD int32) int32 { readFD, err := unix.Dup(int(cReadFD)) if err != nil { - return -1 + return ErrDupReadFD } writeFD, err := unix.Dup(int(cWriteFD)) if err != nil { unix.Close(readFD) - return -1 + return ErrDupWriteFD } conn, err := vpn.NewBidirectionalPipe(uintptr(cReadFD), uintptr(cWriteFD)) if err != nil { unix.Close(readFD) unix.Close(writeFD) - return -1 + return ErrOpenPipe } // Logs will be sent over the protocol @@ -44,7 +51,7 @@ func OpenTunnel(cReadFD, cWriteFD int32) int32 { if err != nil { unix.Close(readFD) unix.Close(writeFD) - return -1 + return ErrNewTunnel } return 0