From 360443c9999c68bf0b70c5d52f8b96b82ee957d2 Mon Sep 17 00:00:00 2001 From: Raphael Tang Date: Mon, 9 May 2022 23:02:41 +0800 Subject: [PATCH] whole lotta changes, mainly adding auto updates (to be completed) --- .github/workflows/main.yml | 43 ++++- .gitignore | 166 ++++++++++++++++++ M1necraft.xcodeproj/project.pbxproj | 21 +++ .../xcshareddata/swiftpm/Package.resolved | 9 + M1necraft/AppCompat.swift | 7 +- M1necraft/AppUpdaterView.swift | 39 ++++ M1necraft/ContentView.swift | 11 +- M1necraft/InstallView.swift | 4 +- M1necraft/M1necraftApp.swift | 11 +- M1necraft/ModInstallHelpView.swift | 4 +- M1necraft/SettingsView.swift | 6 +- M1necraft/SetupView.swift | 5 +- M1necraft/Utils.swift | 6 +- README.md | 12 ++ installer/install.sh | 12 +- requirements.txt | 2 + scripts/update_appcast | 98 +++++++++++ 17 files changed, 434 insertions(+), 22 deletions(-) create mode 100644 M1necraft/AppUpdaterView.swift create mode 100644 requirements.txt create mode 100755 scripts/update_appcast diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index cdc3e65..9699100 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -21,17 +21,28 @@ jobs: action: build configuration: Release - name: (debug) show filetree - run: brew install tree && tree -a && tree -a ~/Library/Developer/Xcode/DerivedData + run: | + brew install tree + tree -a + tree -a ~/Library/Developer/Xcode/DerivedData - name: Compress app bundle run: | mv ~/Library/Developer/Xcode/DerivedData/M1necraft-*/Build/Products/Release/M1necraft.app . zip -r -y M1necraft.app.zip M1necraft.app + - name: Copy Sparkle distribution archive + run: | + cp ~/Library/Developer/Xcode/DerivedData/M1necraft-*/SourcePackages/artifacts/sparkle . - name: Upload build artifacts uses: actions/upload-artifact@v2 with: name: build_artifacts path: ${{ github.workspace }}/M1necraft.app.zip retention-days: 1 + - name: Upload Sparkle tools + with: + name: sparkle_updater + path: ${{ github.workspace }}/sparkle + retention-days: 1 release: needs: build runs-on: macos-latest @@ -45,6 +56,36 @@ jobs: with: files: ${{ github.workspace }}/M1necraft.app.zip generate_release_notes: true + update-appcast: + needs: [build, release] + runs-on: macos-latest + env: + APPCAST_WORKDIR: ${{ github.workspace }}/appcast + steps: + - uses: actions/checkout@v3 + - name: Create $APPCAST_WORKDIR directory + run: mkdir $APPCAST_WORKDIR + - uses: actions/download-artifact@v2 + with: + name: sparkle_updater + path: ${{ env.APPCAST_WORKDIR }} + - name: Import Sparkle keys + working-directory: ${{ env.APPCAST_WORKDIR }} + env: + SPARKLE_PRIVATE_KEY: ${{ secrets.SPARKLE_PRIVATE_KEY }} + run: echo "$SPARKLE_PRIVATE_KEY" | ./bin/generate_keys -f /dev/stdin + - name: Generate Appcast + working-directory: ${{ env.APPCAST_WORKDIR }} + env: + GITHUB_TOKEN: ${{ secrets.CUSTOM_GITHUB_TOKEN }} + run: | + pip install -r requirements.txt + ${{ github.workspace }}/scripts/update_appcast + - name: Publish Appcast + uses: JamesIves/github-pages-deploy-action@v4.3.3 + with: + branch: gh-pages + folder: ${{ env.APPCAST_WORKDIR }} bump-homebrew-cask: runs-on: macos-latest steps: diff --git a/.gitignore b/.gitignore index 4ed943c..7d720a4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ +# Script generated files in development +appcast.xml +releases/ + +# External references for my use when developing resources/ MCAppleSilicon/ @@ -115,3 +120,164 @@ fastlane/test_output # https://github.com/johnno1962/injectionforxcode iOSInjectionProject/ + +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/M1necraft.xcodeproj/project.pbxproj b/M1necraft.xcodeproj/project.pbxproj index 25c3fb0..c700c09 100644 --- a/M1necraft.xcodeproj/project.pbxproj +++ b/M1necraft.xcodeproj/project.pbxproj @@ -18,6 +18,8 @@ 643F03BC28201CAD00C0066B /* Path.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643F03BB28201CAD00C0066B /* Path.swift */; }; 643F03C0282034B300C0066B /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 643F03BF282034B300C0066B /* ErrorView.swift */; }; 643F03C92821552800C0066B /* OctoKit in Frameworks */ = {isa = PBXBuildFile; productRef = 643F03C82821552800C0066B /* OctoKit */; }; + 6464838428256EBB004FB9F2 /* Sparkle in Frameworks */ = {isa = PBXBuildFile; productRef = 6464838328256EBB004FB9F2 /* Sparkle */; }; + 64648386282571DF004FB9F2 /* AppUpdaterView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 64648385282571DF004FB9F2 /* AppUpdaterView.swift */; }; 647D4DC927AE905B00EFC1FE /* AppStoreButtonStyle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D4DC827AE905B00EFC1FE /* AppStoreButtonStyle.swift */; }; 647D4DCB27AFC6C900EFC1FE /* Main.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D4DCA27AFC6C900EFC1FE /* Main.swift */; }; 647D4DCD27AFCA2000EFC1FE /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 647D4DCC27AFCA2000EFC1FE /* AppDelegate.swift */; }; @@ -71,6 +73,7 @@ 64243AD5276D9DEE009826F8 /* M1necraftUITestsLaunchTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = M1necraftUITestsLaunchTests.swift; sourceTree = ""; }; 643F03BB28201CAD00C0066B /* Path.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Path.swift; sourceTree = ""; }; 643F03BF282034B300C0066B /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; + 64648385282571DF004FB9F2 /* AppUpdaterView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppUpdaterView.swift; sourceTree = ""; }; 647D4DC827AE905B00EFC1FE /* AppStoreButtonStyle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStoreButtonStyle.swift; sourceTree = ""; }; 647D4DCA27AFC6C900EFC1FE /* Main.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Main.swift; sourceTree = ""; }; 647D4DCC27AFCA2000EFC1FE /* AppDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; @@ -98,6 +101,7 @@ 643F03C92821552800C0066B /* OctoKit in Frameworks */, 64D0F30C27A98EA900713A33 /* Glob in Frameworks */, 643F03B7281EF63800C0066B /* Path in Frameworks */, + 6464838428256EBB004FB9F2 /* Sparkle in Frameworks */, 64CA5E9D2770497400C73E3D /* Alamofire in Frameworks */, 64CA5EA72771DC7800C73E3D /* ZIPFoundation in Frameworks */, ); @@ -151,6 +155,7 @@ 64EAA53E276F546000A032F1 /* Constants.swift */, 64EAA53A276F448E00A032F1 /* LoginView.swift */, 64CA5EA32771C96E00C73E3D /* Utils.swift */, + 64648385282571DF004FB9F2 /* AppUpdaterView.swift */, 643F03BF282034B300C0066B /* ErrorView.swift */, 647D4E1327B172C600EFC1FE /* ModInstallHelpView.swift */, 64243ABB276D9DEE009826F8 /* Assets.xcassets */, @@ -226,6 +231,7 @@ 64D0F30B27A98EA900713A33 /* Glob */, 643F03B6281EF63800C0066B /* Path */, 643F03C82821552800C0066B /* OctoKit */, + 6464838328256EBB004FB9F2 /* Sparkle */, ); productName = M1necraft; productReference = 64243AB4276D9DED009826F8 /* M1necraft.app */; @@ -305,6 +311,7 @@ 64D0F30A27A98EA900713A33 /* XCRemoteSwiftPackageReference "Glob" */, 643F03B5281EF63800C0066B /* XCRemoteSwiftPackageReference "Path" */, 643F03C72821552800C0066B /* XCRemoteSwiftPackageReference "octokit" */, + 6464838228256EBB004FB9F2 /* XCRemoteSwiftPackageReference "Sparkle" */, ); productRefGroup = 64243AB5276D9DED009826F8 /* Products */; projectDirPath = ""; @@ -358,6 +365,7 @@ 64EAA53B276F448E00A032F1 /* LoginView.swift in Sources */, 647D4E1627B17CEF00EFC1FE /* CloseButton.swift in Sources */, 64243ABA276D9DED009826F8 /* ContentView.swift in Sources */, + 64648386282571DF004FB9F2 /* AppUpdaterView.swift in Sources */, 64D0F31027AAB11500713A33 /* SetupView.swift in Sources */, 64EAA53D276F518F00A032F1 /* LauncherView.swift in Sources */, 647D4DCF27AFCECC00EFC1FE /* AppCompat.swift in Sources */, @@ -713,6 +721,14 @@ kind = branch; }; }; + 6464838228256EBB004FB9F2 /* XCRemoteSwiftPackageReference "Sparkle" */ = { + isa = XCRemoteSwiftPackageReference; + repositoryURL = "https://github.com/sparkle-project/Sparkle"; + requirement = { + kind = upToNextMajorVersion; + minimumVersion = 2.0.0; + }; + }; 64CA5E9B2770497400C73E3D /* XCRemoteSwiftPackageReference "Alamofire" */ = { isa = XCRemoteSwiftPackageReference; repositoryURL = "https://github.com/Alamofire/Alamofire"; @@ -750,6 +766,11 @@ package = 643F03C72821552800C0066B /* XCRemoteSwiftPackageReference "octokit" */; productName = OctoKit; }; + 6464838328256EBB004FB9F2 /* Sparkle */ = { + isa = XCSwiftPackageProductDependency; + package = 6464838228256EBB004FB9F2 /* XCRemoteSwiftPackageReference "Sparkle" */; + productName = Sparkle; + }; 64CA5E9C2770497400C73E3D /* Alamofire */ = { isa = XCSwiftPackageProductDependency; package = 64CA5E9B2770497400C73E3D /* XCRemoteSwiftPackageReference "Alamofire" */; diff --git a/M1necraft.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/M1necraft.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index e6bddd8..c28bbd3 100644 --- a/M1necraft.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/M1necraft.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -45,6 +45,15 @@ "version" : "3.1.0" } }, + { + "identity" : "sparkle", + "kind" : "remoteSourceControl", + "location" : "https://github.com/sparkle-project/Sparkle", + "state" : { + "revision" : "286edd1fa22505a9e54d170e9fd07d775ea233f2", + "version" : "2.1.0" + } + }, { "identity" : "zipfoundation", "kind" : "remoteSourceControl", diff --git a/M1necraft/AppCompat.swift b/M1necraft/AppCompat.swift index 7e02a4a..3040e91 100644 --- a/M1necraft/AppCompat.swift +++ b/M1necraft/AppCompat.swift @@ -27,8 +27,13 @@ struct M1necraftAppCompat { class AppDelegateCompat: AppDelegate { var window: NSWindow! + @StateObject var m = ContentView.ViewModel() + @StateObject var updater = UpdaterViewModel() + @MainActor func application(_ application: NSApplication) { - let contentView = ContentView(m: ContentView.ViewModel()) + let contentView = ContentView() + .environmentObject(m) + .environmentObject(updater) window = NSWindow( contentRect: NSRect(x: 0, y: 0, width: 480, height: 300), diff --git a/M1necraft/AppUpdaterView.swift b/M1necraft/AppUpdaterView.swift new file mode 100644 index 0000000..013b577 --- /dev/null +++ b/M1necraft/AppUpdaterView.swift @@ -0,0 +1,39 @@ +// +// AppUpdaterView.swift +// M1necraft +// +// Created by Raphael Tang on 6/5/22. +// + +import SwiftUI +import Sparkle + +final class UpdaterViewModel: ObservableObject { + private let updaterController: SPUStandardUpdaterController + + @Published var canCheckForUpdates = false + + init() { + updaterController = SPUStandardUpdaterController(startingUpdater: true, updaterDelegate: nil, userDriverDelegate: nil) + updaterController.updater.publisher(for: \.canCheckForUpdates).assign(to: &$canCheckForUpdates) + } + + func checkForUpdates() { + updaterController.checkForUpdates(nil) + } +} + +struct AppUpdaterView: View { + @ObservedObject var updater: UpdaterViewModel + + var body: some View { + Button("Check for Updates...", action: updater.checkForUpdates) + .disabled(!updater.canCheckForUpdates) + } +} + +struct AppUpdaterView_Previews: PreviewProvider { + static var previews: some View { + AppUpdaterView(updater: UpdaterViewModel()) + } +} diff --git a/M1necraft/ContentView.swift b/M1necraft/ContentView.swift index be351f9..222dabd 100644 --- a/M1necraft/ContentView.swift +++ b/M1necraft/ContentView.swift @@ -9,7 +9,8 @@ import SwiftUI import OctoKit struct ContentView: View { - @ObservedObject var m: ContentView.ViewModel + @EnvironmentObject var m: ContentView.ViewModel + @EnvironmentObject var updater: UpdaterViewModel @StateObject var resources = ResourcesViewModel() var body: some View { @@ -20,9 +21,9 @@ struct ContentView: View { Text("Loading...") } case .settingUp: - SetupView(contentViewModel: m, resources: resources) + SetupView(resources: resources) case .completed: - InstallView(contentViewModel: m) + InstallView() case .failed(let error): VStack { Text("We've encountered an error during the setup process.") @@ -59,7 +60,7 @@ struct ContentView: View { } } }.sheet(item: $m.activeSheet) { - $0.modalView(viewModel: m) + $0.modalView() } } } @@ -73,6 +74,6 @@ extension ContentView { struct ContentView_Previews: PreviewProvider { static var previews: some View { - ContentView(m: ContentView.ViewModel()) + ContentView() } } diff --git a/M1necraft/InstallView.swift b/M1necraft/InstallView.swift index bc9082a..d981c04 100644 --- a/M1necraft/InstallView.swift +++ b/M1necraft/InstallView.swift @@ -8,7 +8,7 @@ import SwiftUI struct InstallView: View { - @ObservedObject var contentViewModel: ContentView.ViewModel + @EnvironmentObject var contentViewModel: ContentView.ViewModel @StateObject var m = ViewModel() let versionRefreshTimer = Timer.publish(every: 5, on: .main, in: .common).autoconnect() @@ -105,6 +105,6 @@ extension InstallView { struct InstallView_Previews: PreviewProvider { static var previews: some View { - InstallView(contentViewModel: ContentView.ViewModel()) + InstallView() } } diff --git a/M1necraft/M1necraftApp.swift b/M1necraft/M1necraftApp.swift index e1d9f39..93da4f4 100644 --- a/M1necraft/M1necraftApp.swift +++ b/M1necraft/M1necraftApp.swift @@ -13,10 +13,14 @@ struct M1necraftApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @StateObject var m = ContentView.ViewModel() + @StateObject var updater = UpdaterViewModel() var body: some Scene { WindowGroup { - ContentView(m: m).frame(minWidth: 600, minHeight: 400) + ContentView() + .frame(minWidth: 600, minHeight: 400) + .environmentObject(m) + .environmentObject(updater) }.commands { #if DEBUG CommandMenu("Debug") { @@ -28,10 +32,13 @@ struct M1necraftApp: App { MinecraftLauncher.run() } } + CommandGroup(after: .appInfo) { + AppUpdaterView(updater: updater) + } #endif } Settings { - SettingsView(m: m) + SettingsView() } } } diff --git a/M1necraft/ModInstallHelpView.swift b/M1necraft/ModInstallHelpView.swift index 1bf0b5c..dfb8093 100644 --- a/M1necraft/ModInstallHelpView.swift +++ b/M1necraft/ModInstallHelpView.swift @@ -8,7 +8,7 @@ import SwiftUI struct ModInstallHelpView: View { - @StateObject var m: ContentView.ViewModel + @EnvironmentObject var m: ContentView.ViewModel var body: some View { VStack(alignment: .leading, spacing: 8) { @@ -33,6 +33,6 @@ struct ModInstallHelpView: View { struct ModInstallHelpView_Previews: PreviewProvider { static var previews: some View { - ModInstallHelpView(m: ContentView.ViewModel()) + ModInstallHelpView() } } diff --git a/M1necraft/SettingsView.swift b/M1necraft/SettingsView.swift index 408d848..0843ec3 100644 --- a/M1necraft/SettingsView.swift +++ b/M1necraft/SettingsView.swift @@ -8,7 +8,9 @@ import SwiftUI struct SettingsView: View { - @ObservedObject var m: ContentView.ViewModel + @EnvironmentObject var m: ContentView.ViewModel + @EnvironmentObject var updater: UpdaterViewModel + var showCloseButton = false var body: some View { @@ -54,6 +56,6 @@ struct SettingsView: View { struct SettingsView_Previews: PreviewProvider { static var previews: some View { - SettingsView(m: ContentView.ViewModel()) + SettingsView() } } diff --git a/M1necraft/SetupView.swift b/M1necraft/SetupView.swift index 3c1e3ea..129abd5 100644 --- a/M1necraft/SetupView.swift +++ b/M1necraft/SetupView.swift @@ -11,8 +11,9 @@ import ZIPFoundation import Path struct SetupView: View { + @EnvironmentObject var contentViewModel: ContentView.ViewModel + @StateObject var m = ViewModel() - @ObservedObject var contentViewModel: ContentView.ViewModel @ObservedObject var resources: ResourcesViewModel var body: some View { @@ -134,6 +135,6 @@ extension SetupView { struct SetupView_Previews: PreviewProvider { static var previews: some View { - SetupView(contentViewModel: ContentView.ViewModel(), resources: ResourcesViewModel()) + SetupView(resources: ResourcesViewModel()) } } diff --git a/M1necraft/Utils.swift b/M1necraft/Utils.swift index e02318b..e9b4d07 100644 --- a/M1necraft/Utils.swift +++ b/M1necraft/Utils.swift @@ -221,10 +221,10 @@ enum Sheet: Identifiable { } @ViewBuilder - func modalView(viewModel: ContentView.ViewModel) -> some View { + func modalView() -> some View { switch self { - case .modInstallHelp: ModInstallHelpView(m: viewModel) - case .settings: SettingsView(m: viewModel, showCloseButton: true) + case .modInstallHelp: ModInstallHelpView() + case .settings: SettingsView(showCloseButton: true) } } } diff --git a/README.md b/README.md index 03428ea..3dab976 100644 --- a/README.md +++ b/README.md @@ -50,6 +50,18 @@ In this repository, releases are automatically built and published when a new ta After the new tag has been pushed, you'll also need to update the Homebrew formula. Increment the version in the tarball URL and push the changes. +### Appcast generation & release hosting + +The script located in `scripts/update_appcast` downloads all the releases, feeds them into the generate_appcast script (by Sparkle), and corrects the XML output produced by the script so that it downloads from GitHub Releases. This means I can continue to host the releases in GitHub Actions, with analytics as an added bonus, without the need for an external server or s3 object storage. + +To develop for Python in this repo, you can set up a virtual environment. + +```shell +python3 -m venv .venv # create the venv +source .venv/bin/activate # activate the venv +pip install -r requirements.txt # install dependencies +``` + ## Attributions The inspiration for this came from [this gist](https://gist.github.com/tanmayb123/d55b16c493326945385e815453de411a). Credits to [Tanmay Bakshi](https://github.com/tanmayb123) for doing this. diff --git a/installer/install.sh b/installer/install.sh index 77920e5..4f81671 100755 --- a/installer/install.sh +++ b/installer/install.sh @@ -61,13 +61,21 @@ main() { echo -e "\n${GREEN} * Downloading $APP_NAME.zip${CLEAR}" - # Download latest version if [ -n "$INSTALL_VERSION" ]; then - curl -L "https://github.com/raphtlw/m1necraft/releases/download/$INSTALL_VERSION/$APP_NAME.zip" --output "$APP_NAME.zip" + # Download specific version + curl -L "https://github.com/raphtlw/m1necraft/releases/download/v$INSTALL_VERSION/$APP_NAME.zip" --output "$APP_NAME.zip" else + # Download latest version curl -L "https://github.com/raphtlw/m1necraft/releases/latest/download/$APP_NAME.zip" --output "$APP_NAME.zip" fi + # Check if not found + (zip -T "$APP_NAME.zip" >/dev/null 2>&1) || { + echo -e "\n${RED} ERROR: Version not found${CLEAR}" + cleanup + exit 1 + } + echo -e "${GREEN} * Extracting $APP_NAME.zip${CLEAR}" unzip $APP_NAME.zip >/dev/null chmod a+x $APP_NAME/Contents/MacOS/M1necraft diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..7c92279 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,2 @@ +PyGithub==1.55 +wget==3.2 diff --git a/scripts/update_appcast b/scripts/update_appcast new file mode 100755 index 0000000..09dd47f --- /dev/null +++ b/scripts/update_appcast @@ -0,0 +1,98 @@ +#!/usr/bin/env python3 + +from os import getcwd, mkdir, path, environ, system +from pathlib import Path +from shutil import rmtree +from github import Github +from glob import glob +from xml.etree import ElementTree +import wget + + +# Constants +PRODUCTION = "CI" in environ +DEVELOPMENT = not PRODUCTION + +APP_NAME = "M1necraft" +ARCHIVE_PATH = path.join(getcwd(), "releases") +SPARKLE_TOOLS_PATH = ( + getcwd() + if PRODUCTION + else glob( + path.join( + Path.home(), + "Library", + "Developer", + "Xcode", + "DerivedData", + "M1necraft-*", + "SourcePackages", + "artifacts", + "sparkle", + ) + )[0] +) +APPCAST_PATH = "appcast.xml" +RELEASE_DOWNLOAD_URL_PREFIX = "https://github.com/raphtlw/m1necraft/releases/download" + + +def main(): + # Generate archives folder + github_token = environ.get("GITHUB_TOKEN") + assert github_token, "ERROR: GITHUB_TOKEN environment variable not found" + + g = Github(github_token) + + if path.exists(ARCHIVE_PATH): + rmtree(ARCHIVE_PATH) + mkdir(ARCHIVE_PATH) + + for release in g.get_user().get_repo("m1necraft").get_releases(): + for asset in release.get_assets(): + if asset.name == f"{APP_NAME}.app.zip": + ver = release.tag_name.strip("v") + wget.download( + asset.browser_download_url, + f"{path.join(ARCHIVE_PATH, APP_NAME)} {ver}.app.zip", + ) + print() + + # Generate appcast + out = system( + " ".join( + [ + path.join(SPARKLE_TOOLS_PATH, "bin", "generate_appcast"), + ARCHIVE_PATH, + "-o", + APPCAST_PATH, + ] + ) + ) + assert out == 0, "Command executed with return > 0, exiting to be safe." + + # Correct resulting URLs + ElementTree.register_namespace( + "sparkle", "http://www.andymatuschak.org/xml-namespaces/sparkle" + ) + tree = ElementTree.parse(APPCAST_PATH) + for item in tree.findall(".//item"): + enclosure = item.find("enclosure") + assert ( + enclosure is not None + ), f"Unable to find enclosure tag in {APPCAST_PATH} xml" + version_tag = item.find("title") + assert ( + version_tag is not None + ), f"Unable to find version tag in {APPCAST_PATH} xml" + assert version_tag.text + version = version_tag.text + + enclosure.set( + "url", f"{RELEASE_DOWNLOAD_URL_PREFIX}/v{version}/{APP_NAME}.app.zip" + ) + + tree.write(APPCAST_PATH, xml_declaration=True, encoding="UTF-8") + + +if __name__ == "__main__": + main()