diff --git a/.github/workflows/publish-go-tester-task.yml b/.github/workflows/publish-go-tester-task.yml index 69efdba05..3b5f871d0 100644 --- a/.github/workflows/publish-go-tester-task.yml +++ b/.github/workflows/publish-go-tester-task.yml @@ -132,9 +132,9 @@ jobs: - name: Build the Agent for macos env: - MACOSX_DEPLOYMENT_TARGET: 10.11 # minimum supported version for mac - CGO_CFLAGS: -mmacosx-version-min=10.11 - CGO_LDFLAGS: -mmacosx-version-min=10.11 + MACOSX_DEPLOYMENT_TARGET: 10.15 # minimum supported version for mac + CGO_CFLAGS: -mmacosx-version-min=10.15 + CGO_LDFLAGS: -mmacosx-version-min=10.15 run: task go:build if: runner.os == 'macOS' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9e6837e56..f9f8b6910 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -8,7 +8,7 @@ on: env: # As defined by the Taskfile's PROJECT_NAME variable PROJECT_NAME: arduino-create-agent - TARGET: "/CreateAgent/Stable" + TARGET: "/CreateAgent/Stable/" OLD_TARGET: "/CreateBridge/" # compatibility with older releases (we can't change config.ini) VERSION_TARGET: "arduino-create-static/agent-metadata/" AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} @@ -26,6 +26,8 @@ env: jobs: # The build job is responsible for: configuring the environment, testing and compiling process build: + outputs: + prerelease: ${{ steps.prerelease.outputs.IS_PRE }} strategy: matrix: os: [ubuntu-20.04, windows-2019, macos-12] @@ -110,9 +112,9 @@ jobs: - name: Build the Agent for macos env: - MACOSX_DEPLOYMENT_TARGET: 10.11 # minimum supported version for mac - CGO_CFLAGS: -mmacosx-version-min=10.11 - CGO_LDFLAGS: -mmacosx-version-min=10.11 + MACOSX_DEPLOYMENT_TARGET: 10.15 # minimum supported version for mac + CGO_CFLAGS: -mmacosx-version-min=10.15 + CGO_LDFLAGS: -mmacosx-version-min=10.15 run: task go:build if: matrix.os == 'macos-12' @@ -121,6 +123,14 @@ jobs: run: go-selfupdate ${{ env.PROJECT_NAME }}${{ matrix.ext }} ${TAG_VERSION} if: matrix.arch != '-386' && steps.prerelease.outputs.IS_PRE != 'true' + # for now we do not distribute m1 build, this is a workaround for now + - name: Copy autoupdate file for darwin-arm64 (m1 arch) + working-directory: public/ + run: | + cp darwin-amd64.json darwin-arm64.json + cp ${TAG_VERSION}/darwin-amd64.gz ${TAG_VERSION}/darwin-arm64.gz + if: matrix.os == `macos-12` && steps.prerelease.outputs.IS_PRE != 'true' + - name: Create autoupdate files for win32 run: go-selfupdate -platform windows${{ matrix.arch }} ${{ env.PROJECT_NAME }}${{ matrix.ext }} ${TAG_VERSION} if: matrix.arch == '-386' && matrix.os == 'windows-2019' && steps.prerelease.outputs.IS_PRE != 'true' @@ -142,6 +152,11 @@ jobs: create-macos-bundle: needs: build + # for not they are exaclty the same + strategy: + matrix: + arch: [amd64, arm64] + runs-on: macos-12 env: EXE_PATH: "skel/ArduinoCreateAgent.app/Contents/MacOS/" @@ -156,7 +171,7 @@ jobs: - name: Download artifact uses: actions/download-artifact@v3 with: - name: ${{ env.PROJECT_NAME }}-macos-12-amd64 + name: ${{ env.PROJECT_NAME }}-macos-12-amd64 # if we want to support darwin-arm64 in the future for real this has to change. path: ${{ env.EXE_PATH }} - name: Remove placeholder file @@ -195,31 +210,37 @@ jobs: EOF - name: Tar bundle to keep permissions - run: tar -cvf ArduinoCreateAgent.app.tar -C skel/ . + run: tar -cvf ArduinoCreateAgent.app_${{ matrix.arch }}.tar -C skel/ . - name: Upload artifacts uses: actions/upload-artifact@v3 with: if-no-files-found: error - name: ArduinoCreateAgent.app - path: ArduinoCreateAgent.app.tar + name: ArduinoCreateAgent.app_${{ matrix.arch }} + path: ArduinoCreateAgent.app_${{ matrix.arch }}.tar - # The notarize-macos job will download the macos bundle from the previous job, sign, notarize and re-upload it. + # The notarize-macos job will download the macos bundle from the previous job, sign, notarize and re-upload it, uploading it also on s3 download servers for the autoupdate. notarize-macos: name: Notarize bundle + + # for not they are exaclty the same + strategy: + matrix: + arch: [amd64, arm64] + runs-on: macos-12 env: GON_PATH: ${{ github.workspace }}/gon - needs: create-macos-bundle + needs: [build, create-macos-bundle] steps: - name: Download artifact uses: actions/download-artifact@v3 with: - name: ArduinoCreateAgent.app + name: ArduinoCreateAgent.app_${{ matrix.arch }} - name: un-Tar bundle - run: tar -xvf ArduinoCreateAgent.app.tar + run: tar -xvf ArduinoCreateAgent.app_${{ matrix.arch }}.tar - name: Import Code-Signing Certificates run: | @@ -270,18 +291,37 @@ jobs: # Ask Gon for zip output to force notarization process to take place. # The CI will upload the zip output zip { - output_path = "ArduinoCreateAgent.app_notarized.zip" + output_path = "ArduinoCreateAgent.app_${{ matrix.arch }}_notarized.zip" } EOF - name: Sign and notarize binary run: gon -log-level=debug -log-json "${{ env.GON_CONFIG_PATH }}" + + - name: Upload autoupdate bundle to Arduino downloads servers + run: aws s3 cp ArduinoCreateAgent.app_${{ matrix.arch }}_notarized.zip s3://${{ secrets.DOWNLOADS_BUCKET }}${{ env.TARGET }}${GITHUB_REF/refs\/tags\//}/ # the version should be created in th the build job + if: ${{ needs.build.outputs.prerelease != 'true' }} + + - name: Generate json file used for the new autoupdate + run: | + cat > darwin-${{ matrix.arch }}-bundle.json </tmp/semver && chmod +x /tmp/semver - if [[ $(/tmp/semver get prerel ${GITHUB_REF/refs\/tags\//}) ]]; then echo "IS_PRE=true" >> $GITHUB_OUTPUT; fi - # mandatory step because upload-release-action does not support multiple folders - name: prepare artifacts for the release run: | @@ -562,7 +595,7 @@ jobs: release_name: ${{ github.ref }} body: ${{ steps.release_body.outputs.RBODY}} draft: false - prerelease: ${{ steps.prerelease.outputs.IS_PRE }} + prerelease: ${{ needs.build.outputs.prerelease }} - name: Upload release files on Github uses: svenstaro/upload-release-action@v2 @@ -574,10 +607,10 @@ jobs: - name: Upload release files on Arduino downloads servers run: aws s3 sync release/ s3://${{ secrets.DOWNLOADS_BUCKET }}${{ env.TARGET }} - if: steps.prerelease.outputs.IS_PRE != 'true' + if: ${{ needs.build.outputs.prerelease != 'true' }} - name: Update version file (used by frontend to trigger autoupdate and create filename) run: | echo {\"Version\": \"${GITHUB_REF##*/}\"} > /tmp/agent-version.json aws s3 cp /tmp/agent-version.json s3://${{ env.VERSION_TARGET }} - if: steps.prerelease.outputs.IS_PRE != 'true' + if: ${{ needs.build.outputs.prerelease != 'true' }} diff --git a/update.go b/update.go index 21569f36c..c50f53f38 100644 --- a/update.go +++ b/update.go @@ -41,5 +41,9 @@ func updateHandler(c *gin.Context) { return } c.JSON(200, gin.H{"success": "Please wait a moment while the agent reboots itself"}) - Systray.RestartWith(restartPath) + if restartPath == "quit" { + Systray.Quit() + } else { + Systray.RestartWith(restartPath) + } } diff --git a/updater/updater_darwin.go b/updater/updater_darwin.go index 45c67e5c7..2610a62dd 100644 --- a/updater/updater_darwin.go +++ b/updater/updater_darwin.go @@ -15,10 +15,142 @@ package updater +/* +#cgo CFLAGS: -x objective-c +#cgo LDFLAGS: -framework Cocoa +#import + +void runApplication(const char *path) { + NSWorkspace *ws = [NSWorkspace sharedWorkspace]; + NSURL *url = [NSURL fileURLWithPath:@(path) isDirectory:NO]; + + NSWorkspaceOpenConfiguration* configuration = [NSWorkspaceOpenConfiguration new]; + //[configuration setEnvironment:env]; + [configuration setPromptsUserIfNeeded:YES]; + [configuration setCreatesNewApplicationInstance:YES]; + + dispatch_semaphore_t semaphore = dispatch_semaphore_create(0); + [ws openApplicationAtURL:url configuration:configuration completionHandler:^(NSRunningApplication* app, NSError* error) { + dispatch_semaphore_signal(semaphore); + }]; + dispatch_semaphore_wait(semaphore, DISPATCH_TIME_FOREVER); +} +*/ +import "C" + +import ( + "bytes" + "context" + "crypto/sha256" + "fmt" + "io" + "os" + + "github.com/arduino/go-paths-helper" + "github.com/codeclysm/extract/v3" + "github.com/sirupsen/logrus" +) + func start(src string) string { return "" } func checkForUpdates(currentVersion string, updateAPIURL, updateBinURL string, cmdName string) (string, error) { - return "", nil + executablePath, err := os.Executable() + if err != nil { + return "", fmt.Errorf("could not app path: %w", err) + } + currentAppPath := paths.New(executablePath).Parent().Parent().Parent() + if currentAppPath.Ext() != ".app" { + return "", fmt.Errorf("could not find app root in %s", executablePath) + } + oldAppPath := currentAppPath.Parent().Join("ArdiunoCreateAgent.old.app") + if oldAppPath.Exist() { + return "", fmt.Errorf("temp app already exists: %s, cannot update", oldAppPath) + } + + // Fetch information about updates + info, err := fetchInfo(updateAPIURL, cmdName) + if err != nil { + return "", err + } + if info.Version == currentVersion { + // No updates available, bye bye + return "", nil + } + + tmp := paths.TempDir().Join("arduino-create-agent") + if err := tmp.MkdirAll(); err != nil { + return "", err + } + tmpZip := tmp.Join("update.zip") + tmpAppPath := tmp.Join("ArduinoCreateAgent-update.app") + defer tmp.RemoveAll() + + // Download the update. + downloadURL := updateBinURL + cmdName + "/" + info.Version + "/ArduinoCreateAgent.app_notarized.zip" + logrus.WithField("url", downloadURL).Info("Downloading update") + download, err := fetch(downloadURL) + if err != nil { + return "", err + } + defer download.Close() + + f, err := tmpZip.Create() + if err != nil { + return "", err + } + defer f.Close() + + sha := sha256.New() + if _, err := io.Copy(io.MultiWriter(sha, f), download); err != nil { + return "", err + } + f.Close() + + // Check the hash + if s := sha.Sum(nil); !bytes.Equal(s, info.Sha256) { + return "", fmt.Errorf("bad hash: %s (expected %s)", s, info.Sha256) + } + + // Unzip the update + logrus.WithField("tmpDir", tmpAppPath).Info("Unzipping update") + if err := tmpAppPath.MkdirAll(); err != nil { + return "", fmt.Errorf("could not create tmp dir to unzip update: %w", err) + } + + f, err = tmpZip.Open() + if err != nil { + return "", fmt.Errorf("could not open archive for unzip: %w", err) + } + defer f.Close() + if err := extract.Archive(context.Background(), f, tmpAppPath.String(), nil); err != nil { + return "", fmt.Errorf("extracting archive: %w", err) + } + + // Rename current app as .old + logrus.WithField("from", currentAppPath).WithField("to", oldAppPath).Info("Renaming old app") + if err := currentAppPath.Rename(oldAppPath); err != nil { + return "", fmt.Errorf("could not rename old app as .old: %w", err) + } + + // Install new app + logrus.WithField("from", tmpAppPath).WithField("to", currentAppPath).Info("Copying updated app") + if err := tmpAppPath.CopyDirTo(currentAppPath); err != nil { + // Try rollback changes + _ = currentAppPath.RemoveAll() + _ = oldAppPath.Rename(currentAppPath) + return "", fmt.Errorf("could not install app: %w", err) + } + + // Remove old app + logrus.WithField("to", oldAppPath).Info("Removing old app") + _ = oldAppPath.RemoveAll() + + // Restart agent + logrus.WithField("path", currentAppPath).Info("Running new app") + C.runApplication(C.CString(currentAppPath.String())) + + // Close old agent + return "quit", nil }