mirror of
https://github.com/nextcloud/desktop.git
synced 2025-07-21 17:32:29 +00:00
feat(mac-crafter): Update to Swift 6.1 and Strict Concurrency Checks.
- Isolated global state into an actor. - Updated declarations and calls to reflect asynchronicity. - The replacement of the enumerator with subpathsOfDirectory(atPath:) was suggested by Copilot to necessarily avoid the use of synchronous enumeration in an asynchronous context. - Updated README. Signed-off-by: Iva Horn <iva.horn@icloud.com>
This commit is contained in:
@ -1,4 +1,4 @@
|
||||
// swift-tools-version: 5.10
|
||||
// swift-tools-version: 6.1
|
||||
// The swift-tools-version declares the minimum version of Swift required to build this package.
|
||||
|
||||
/*
|
||||
@ -21,7 +21,10 @@ let package = Package(
|
||||
// Targets can depend on other targets in this package and products from dependencies.
|
||||
.executableTarget(
|
||||
name: "mac-crafter",
|
||||
dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")]
|
||||
dependencies: [.product(name: "ArgumentParser", package: "swift-argument-parser")],
|
||||
swiftSettings: [
|
||||
.enableUpcomingFeature("StrictConcurrency")
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
|
@ -4,16 +4,18 @@
|
||||
-->
|
||||
# mac-crafter
|
||||
|
||||
mac-crafter is a tool to easily build a fully functional Nextcloud Desktop Client for macOS. It automates cloning, configuring, crafting, codesigning, packaging, and even DMG creation of the client. The tool is built with Swift’s ArgumentParser, and it drives the KDE Craft build system, along with some Python scripts and shell commands.
|
||||
mac-crafter is a tool to easily build a fully functional Nextcloud Desktop Client for macOS.
|
||||
It automates cloning, configuring, crafting, codesigning, packaging, and even DMG creation of the client.
|
||||
The tool is built with Swift’s ArgumentParser and it drives the KDE Craft build system along with some Python scripts and shell commands.
|
||||
|
||||
## System Requirements
|
||||
|
||||
- macOS > 11
|
||||
- macOS 11 Big Sur or newer
|
||||
- Xcode
|
||||
- Python3
|
||||
- Homebrew (for installing additional tools like `inkscape`, `pyenv`, and `create-dmg`)
|
||||
|
||||
## Installation & Setup
|
||||
## Installation
|
||||
|
||||
After cloning the Nextcloud Desktop Client repository, navigate to the `admin/osx/mac-crafter` directory and run:
|
||||
|
||||
@ -21,9 +23,10 @@ After cloning the Nextcloud Desktop Client repository, navigate to the `admin/os
|
||||
swift run mac-crafter
|
||||
```
|
||||
|
||||
This will automatically check for the required tools and install them if they are missing. The script will also clone the KDE Craft repository if it is not already present.
|
||||
This will automatically check for the required tools and install them if they are missing.
|
||||
The script will also clone the KDE Craft repository if it is not already present.
|
||||
|
||||
## Commands & Options
|
||||
## Usage
|
||||
|
||||
mac-crafter comes with several subcommands:
|
||||
|
||||
|
30
admin/osx/mac-crafter/Sources/State.swift
Normal file
30
admin/osx/mac-crafter/Sources/State.swift
Normal file
@ -0,0 +1,30 @@
|
||||
// SPDX-FileCopyrightText: Nextcloud GmbH
|
||||
// SPDX-FileCopyrightText: 2025 Iva Horn
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import Foundation
|
||||
|
||||
///
|
||||
/// Global state object.
|
||||
///
|
||||
actor State {
|
||||
weak private var process: Process?
|
||||
|
||||
static let shared = State()
|
||||
|
||||
///
|
||||
/// Register the shell command process.
|
||||
///
|
||||
func register(_ process: Process) {
|
||||
self.process = process
|
||||
}
|
||||
|
||||
///
|
||||
/// Terminate any previously registered shell command process.
|
||||
///
|
||||
/// Silently fails in case no process is registered.
|
||||
///
|
||||
func terminate() {
|
||||
process?.terminate()
|
||||
}
|
||||
}
|
@ -24,7 +24,7 @@ func isAppExtension(_ path: String) -> Bool {
|
||||
path.hasSuffix(".appex")
|
||||
}
|
||||
|
||||
func isExecutable(_ path: String) throws -> Bool {
|
||||
func isExecutable(_ path: String) async throws -> Bool {
|
||||
let outPipe = Pipe()
|
||||
let errPipe = Pipe()
|
||||
let task = Process()
|
||||
@ -32,7 +32,7 @@ func isExecutable(_ path: String) throws -> Bool {
|
||||
task.standardError = errPipe
|
||||
|
||||
let command = "file \"\(path)\""
|
||||
guard run("/bin/zsh", ["-c", command], task: task) == 0 else {
|
||||
guard await run("/bin/zsh", ["-c", command], task: task) == 0 else {
|
||||
throw CodeSigningError.failedToCodeSign("Failed to determine if \(path) is an executable.")
|
||||
}
|
||||
|
||||
@ -43,11 +43,11 @@ func isExecutable(_ path: String) throws -> Bool {
|
||||
return output.contains("Mach-O 64-bit executable")
|
||||
}
|
||||
|
||||
func codesign(identity: String, path: String, options: String = defaultCodesignOptions) throws {
|
||||
func codesign(identity: String, path: String, options: String = defaultCodesignOptions) async throws {
|
||||
print("Code-signing \(path)...")
|
||||
let command = "codesign -s \"\(identity)\" \(options) \"\(path)\""
|
||||
for _ in 1...5 {
|
||||
guard shell(command) == 0 else {
|
||||
guard await shell(command) == 0 else {
|
||||
print("Code-signing failed, retrying ...")
|
||||
continue
|
||||
}
|
||||
@ -64,35 +64,38 @@ func recursivelyCodesign(
|
||||
identity: String,
|
||||
options: String = defaultCodesignOptions,
|
||||
skip: [String] = []
|
||||
) throws {
|
||||
) async throws {
|
||||
let fm = FileManager.default
|
||||
guard fm.fileExists(atPath: path) else {
|
||||
throw AppBundleSigningError.doesNotExist("Item at \(path) does not exist.")
|
||||
}
|
||||
|
||||
guard let pathEnumerator = fm.enumerator(atPath: path) else {
|
||||
let enumeratedItems: [String]
|
||||
do {
|
||||
enumeratedItems = try fm.subpathsOfDirectory(atPath: path)
|
||||
} catch {
|
||||
throw AppBundleSigningError.couldNotEnumerate(
|
||||
"Failed to enumerate directory at \(path)."
|
||||
)
|
||||
}
|
||||
|
||||
for case let enumeratedItem as String in pathEnumerator {
|
||||
for enumeratedItem in enumeratedItems {
|
||||
let enumeratedItemPath = "\(path)/\(enumeratedItem)"
|
||||
guard !skip.contains(enumeratedItemPath) else {
|
||||
print("Skipping \(enumeratedItemPath)...")
|
||||
continue
|
||||
}
|
||||
let isExecutableFile = try isExecutable(enumeratedItemPath)
|
||||
let isExecutableFile = try await isExecutable(enumeratedItemPath)
|
||||
guard isLibrary(enumeratedItem) || isAppExtension(enumeratedItem) || isExecutableFile else {
|
||||
continue
|
||||
}
|
||||
try codesign(identity: identity, path: enumeratedItemPath, options: options)
|
||||
try await codesign(identity: identity, path: enumeratedItemPath, options: options)
|
||||
}
|
||||
}
|
||||
|
||||
func saveCodesignEntitlements(target: String, path: String) throws {
|
||||
func saveCodesignEntitlements(target: String, path: String) async throws {
|
||||
let command = "codesign -d --entitlements \"\(path)\" --xml \"\(target)\""
|
||||
guard shell(command) == 0 else {
|
||||
guard await shell(command) == 0 else {
|
||||
throw CodeSigningError.failedToCodeSign("Failed to save entitlements for \(target).")
|
||||
}
|
||||
}
|
||||
@ -101,26 +104,26 @@ func codesignClientAppBundle(
|
||||
at clientAppDir: String,
|
||||
withCodeSignIdentity codeSignIdentity: String,
|
||||
usingEntitlements entitlementsPath: String? = nil
|
||||
) throws {
|
||||
) async throws {
|
||||
print("Code-signing Nextcloud Desktop Client libraries, frameworks and plugins...")
|
||||
|
||||
let clientContentsDir = "\(clientAppDir)/Contents"
|
||||
let frameworksPath = "\(clientContentsDir)/Frameworks"
|
||||
let pluginsPath = "\(clientContentsDir)/PlugIns"
|
||||
|
||||
try recursivelyCodesign(path: frameworksPath, identity: codeSignIdentity)
|
||||
try recursivelyCodesign(path: pluginsPath, identity: codeSignIdentity)
|
||||
try recursivelyCodesign(path: "\(clientContentsDir)/Resources", identity: codeSignIdentity)
|
||||
try await recursivelyCodesign(path: frameworksPath, identity: codeSignIdentity)
|
||||
try await recursivelyCodesign(path: pluginsPath, identity: codeSignIdentity)
|
||||
try await recursivelyCodesign(path: "\(clientContentsDir)/Resources", identity: codeSignIdentity)
|
||||
|
||||
print("Code-signing QtWebEngineProcess...")
|
||||
let qtWebEngineProcessPath =
|
||||
"\(frameworksPath)/QtWebEngineCore.framework/Versions/A/Helpers/QtWebEngineProcess.app"
|
||||
try codesign(identity: codeSignIdentity,
|
||||
try await codesign(identity: codeSignIdentity,
|
||||
path: qtWebEngineProcessPath,
|
||||
options: "--timestamp --force --verbose=4 --options runtime --deep --entitlements \"\(qtWebEngineProcessPath)/Contents/Resources/QtWebEngineProcess.entitlements\"")
|
||||
|
||||
print("Code-signing QtWebEngine...")
|
||||
try codesign(identity: codeSignIdentity, path: "\(frameworksPath)/QtWebEngineCore.framework")
|
||||
try await codesign(identity: codeSignIdentity, path: "\(frameworksPath)/QtWebEngineCore.framework")
|
||||
|
||||
// Time to fix notarisation issues.
|
||||
// Multiple components of the app will now have the get-task-allow entitlements.
|
||||
@ -129,27 +132,27 @@ func codesignClientAppBundle(
|
||||
let sparkleFrameworkPath = "\(frameworksPath)/Sparkle.framework"
|
||||
if FileManager.default.fileExists(atPath: sparkleFrameworkPath) {
|
||||
print("Code-signing Sparkle...")
|
||||
try codesign(
|
||||
try await codesign(
|
||||
identity: codeSignIdentity,
|
||||
path: "\(sparkleFrameworkPath)/Versions/B/XPCServices/Installer.xpc",
|
||||
options: "-f -o runtime"
|
||||
)
|
||||
try codesign(
|
||||
try await codesign(
|
||||
identity: codeSignIdentity,
|
||||
path: "\(sparkleFrameworkPath)/Versions/B/XPCServices/Downloader.xpc",
|
||||
options: "-f -o runtime --preserve-metadata=entitlements"
|
||||
)
|
||||
try codesign(
|
||||
try await codesign(
|
||||
identity: codeSignIdentity,
|
||||
path: "\(sparkleFrameworkPath)/Versions/B/Autoupdate",
|
||||
options: "-f -o runtime"
|
||||
)
|
||||
try codesign(
|
||||
try await codesign(
|
||||
identity: codeSignIdentity,
|
||||
path: "\(sparkleFrameworkPath)/Versions/B/Updater.app",
|
||||
options: "-f -o runtime"
|
||||
)
|
||||
try codesign(
|
||||
try await codesign(
|
||||
identity: codeSignIdentity, path: sparkleFrameworkPath, options: "-f -o runtime"
|
||||
)
|
||||
} else {
|
||||
@ -164,7 +167,7 @@ func codesignClientAppBundle(
|
||||
let appExtensionPath = "\(pluginsPath)/\(appExtension)"
|
||||
let tmpEntitlementXmlPath =
|
||||
fm.temporaryDirectory.appendingPathComponent(UUID().uuidString).path.appending(".xml")
|
||||
try saveCodesignEntitlements(target: appExtensionPath, path: tmpEntitlementXmlPath)
|
||||
try await saveCodesignEntitlements(target: appExtensionPath, path: tmpEntitlementXmlPath)
|
||||
// Strip the get-task-allow entitlement from the XML entitlements file
|
||||
let xmlEntitlements = try String(contentsOfFile: tmpEntitlementXmlPath)
|
||||
let entitlementKeyValuePair = "<key>com.apple.security.get-task-allow</key><true/>"
|
||||
@ -173,7 +176,7 @@ func codesignClientAppBundle(
|
||||
try strippedEntitlements.write(toFile: tmpEntitlementXmlPath,
|
||||
atomically: true,
|
||||
encoding: .utf8)
|
||||
try codesign(identity: codeSignIdentity,
|
||||
try await codesign(identity: codeSignIdentity,
|
||||
path: appExtensionPath,
|
||||
options: "--timestamp --force --verbose=4 --options runtime --deep --entitlements \"\(tmpEntitlementXmlPath)\"")
|
||||
}
|
||||
@ -189,14 +192,14 @@ func codesignClientAppBundle(
|
||||
// Sign the main executable last
|
||||
let mainExecutableName = String(appName.dropLast(".app".count))
|
||||
let mainExecutablePath = "\(binariesDir)/\(mainExecutableName)"
|
||||
try recursivelyCodesign(path: binariesDir, identity: codeSignIdentity, skip: [mainExecutablePath])
|
||||
try await recursivelyCodesign(path: binariesDir, identity: codeSignIdentity, skip: [mainExecutablePath])
|
||||
|
||||
var mainExecutableCodesignOptions = defaultCodesignOptions
|
||||
if let entitlementsPath {
|
||||
mainExecutableCodesignOptions =
|
||||
"--timestamp --force --verbose=4 --options runtime --entitlements \"\(entitlementsPath)\""
|
||||
}
|
||||
try codesign(
|
||||
try await codesign(
|
||||
identity: codeSignIdentity, path: mainExecutablePath, options: mainExecutableCodesignOptions
|
||||
)
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Nextcloud GmbH
|
||||
// SPDX-FileCopyrightText: 2024 Claudio Cambra
|
||||
// SPDX-FileCopyrightText: 2025 Iva Horn
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import Foundation
|
||||
|
||||
@ -27,12 +27,12 @@ func installIfMissing(
|
||||
_ command: String,
|
||||
_ installCommand: String,
|
||||
installCommandEnv: [String: String]? = nil
|
||||
) throws {
|
||||
if commandExists(command) {
|
||||
) async throws {
|
||||
if await commandExists(command) {
|
||||
print("\(command) is installed.")
|
||||
} else {
|
||||
print("\(command) is missing. Installing...")
|
||||
guard shell(installCommand, env: installCommandEnv) == 0 else {
|
||||
guard await shell(installCommand, env: installCommandEnv) == 0 else {
|
||||
throw InstallError.failedToInstall("Failed to install \(command).")
|
||||
}
|
||||
print("\(command) installed.")
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Nextcloud GmbH
|
||||
// SPDX-FileCopyrightText: 2024 Claudio Cambra
|
||||
// SPDX-FileCopyrightText: 2025 Iva Horn
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import Foundation
|
||||
|
||||
@ -16,22 +16,22 @@ enum PackagingError: Error {
|
||||
}
|
||||
|
||||
/// NOTE: Requires Packages utility. http://s.sudre.free.fr/Software/Packages/about.html
|
||||
fileprivate func buildPackage(appName: String, buildWorkPath: String, productPath: String) throws -> String {
|
||||
fileprivate func buildPackage(appName: String, buildWorkPath: String, productPath: String) async throws -> String {
|
||||
let packageFile = "\(appName).pkg"
|
||||
let pkgprojPath = "\(buildWorkPath)/admin/osx/macosx.pkgproj"
|
||||
|
||||
guard shell("packagesutil --file \(pkgprojPath) set project name \(appName)") == 0 else {
|
||||
guard await shell("packagesutil --file \(pkgprojPath) set project name \(appName)") == 0 else {
|
||||
throw PackagingError.projectNameSettingError("Could not set project name in pkgproj!")
|
||||
}
|
||||
guard shell("packagesbuild -v --build-folder \(productPath) -F \(productPath) \(pkgprojPath)") == 0 else {
|
||||
guard await shell("packagesbuild -v --build-folder \(productPath) -F \(productPath) \(pkgprojPath)") == 0 else {
|
||||
throw PackagingError.packageBuildError("Error building pkg file!")
|
||||
}
|
||||
return "\(productPath)/\(packageFile)"
|
||||
}
|
||||
|
||||
fileprivate func signPackage(packagePath: String, packageSigningId: String) throws {
|
||||
fileprivate func signPackage(packagePath: String, packageSigningId: String) async throws {
|
||||
let packagePathNew = "\(packagePath).new"
|
||||
guard shell("productsign --timestamp --sign '\(packageSigningId)' \(packagePath) \(packagePathNew)") == 0 else {
|
||||
guard await shell("productsign --timestamp --sign '\(packageSigningId)' \(packagePath) \(packagePathNew)") == 0 else {
|
||||
throw PackagingError.packageSigningError("Could not sign pkg file!")
|
||||
}
|
||||
let fm = FileManager.default
|
||||
@ -41,25 +41,25 @@ fileprivate func signPackage(packagePath: String, packageSigningId: String) thro
|
||||
|
||||
fileprivate func notarisePackage(
|
||||
packagePath: String, appleId: String, applePassword: String, appleTeamId: String
|
||||
) throws {
|
||||
guard shell("xcrun notarytool submit \(packagePath) --apple-id \(appleId) --password \(applePassword) --team-id \(appleTeamId) --wait") == 0 else {
|
||||
) async throws {
|
||||
guard await shell("xcrun notarytool submit \(packagePath) --apple-id \(appleId) --password \(applePassword) --team-id \(appleTeamId) --wait") == 0 else {
|
||||
throw PackagingError.packageNotarisationError("Failure when notarising package!")
|
||||
}
|
||||
guard shell("xcrun stapler staple \(packagePath)") == 0 else {
|
||||
guard await shell("xcrun stapler staple \(packagePath)") == 0 else {
|
||||
throw PackagingError.packageNotarisationError("Could not staple notarisation on package!")
|
||||
}
|
||||
}
|
||||
|
||||
fileprivate func buildSparklePackage(packagePath: String, buildPath: String) throws -> String {
|
||||
fileprivate func buildSparklePackage(packagePath: String, buildPath: String) async throws -> String {
|
||||
let sparkleTbzPath = "\(packagePath).tbz"
|
||||
guard shell("tar cf \(sparkleTbzPath) \(packagePath)") == 0 else {
|
||||
guard await shell("tar cf \(sparkleTbzPath) \(packagePath)") == 0 else {
|
||||
throw PackagingError.packageSparkleBuildError("Could not create Sparkle package tbz!")
|
||||
}
|
||||
return sparkleTbzPath
|
||||
}
|
||||
|
||||
fileprivate func signSparklePackage(sparkleTbzPath: String, buildPath: String, signKey: String) throws {
|
||||
guard shell("\(buildPath)/bin/sign_update -s \(signKey) \(sparkleTbzPath)") == 0 else {
|
||||
fileprivate func signSparklePackage(sparkleTbzPath: String, buildPath: String, signKey: String) async throws {
|
||||
guard await shell("\(buildPath)/bin/sign_update -s \(signKey) \(sparkleTbzPath)") == 0 else {
|
||||
throw PackagingError.packageSparkleSignError("Could not sign Sparkle package tbz!")
|
||||
}
|
||||
}
|
||||
@ -75,10 +75,10 @@ func packageAppBundle(
|
||||
applePassword: String?,
|
||||
appleTeamId: String?,
|
||||
sparklePackageSignKey: String?
|
||||
) throws {
|
||||
) async throws {
|
||||
print("Creating pkg file for client…")
|
||||
let buildWorkPath = "\(buildPath)/\(craftTarget)/build/\(craftBlueprintName)/work/build"
|
||||
let packagePath = try buildPackage(
|
||||
let packagePath = try await buildPackage(
|
||||
appName: appName,
|
||||
buildWorkPath: buildWorkPath,
|
||||
productPath: productPath
|
||||
@ -86,11 +86,11 @@ func packageAppBundle(
|
||||
|
||||
if let packageSigningId {
|
||||
print("Signing pkg with \(packageSigningId)…")
|
||||
try signPackage(packagePath: packagePath, packageSigningId: packageSigningId)
|
||||
try await signPackage(packagePath: packagePath, packageSigningId: packageSigningId)
|
||||
|
||||
if let appleId, let applePassword, let appleTeamId {
|
||||
print("Notarising pkg with Apple ID \(appleId)…")
|
||||
try notarisePackage(
|
||||
try await notarisePackage(
|
||||
packagePath: packagePath,
|
||||
appleId: appleId,
|
||||
applePassword: applePassword,
|
||||
@ -101,11 +101,11 @@ func packageAppBundle(
|
||||
|
||||
print("Creating Sparkle TBZ file…")
|
||||
let sparklePackagePath =
|
||||
try buildSparklePackage(packagePath: packagePath, buildPath: buildPath)
|
||||
try await buildSparklePackage(packagePath: packagePath, buildPath: buildPath)
|
||||
|
||||
if let sparklePackageSignKey {
|
||||
print("Signing Sparkle TBZ file…")
|
||||
try signSparklePackage(
|
||||
try await signSparklePackage(
|
||||
sparkleTbzPath: sparklePackagePath,
|
||||
buildPath: buildPath,
|
||||
signKey: sparklePackageSignKey
|
||||
@ -123,7 +123,7 @@ func createDmgForAppBundle(
|
||||
applePassword: String?,
|
||||
appleTeamId: String?,
|
||||
sparklePackageSignKey: String?
|
||||
) throws {
|
||||
) async throws {
|
||||
print("Creating DMG for the client…")
|
||||
|
||||
let dmgFilePath = URL(fileURLWithPath: productPath)
|
||||
@ -131,17 +131,17 @@ func createDmgForAppBundle(
|
||||
.appendingPathExtension("dmg")
|
||||
.path
|
||||
|
||||
guard shell("create-dmg --volname \(appName) --filesystem APFS --app-drop-link 513 37 --window-size 787 276 \"\(dmgFilePath)\" \"\(appBundlePath)\"") == 0 else {
|
||||
guard await shell("create-dmg --volname \(appName) --filesystem APFS --app-drop-link 513 37 --window-size 787 276 \"\(dmgFilePath)\" \"\(appBundlePath)\"") == 0 else {
|
||||
throw PackagingError.packageCreateDmgFailed("Command failed.")
|
||||
}
|
||||
|
||||
if let packageSigningId {
|
||||
print("Signing DMG with \(packageSigningId)…")
|
||||
try codesign(identity: packageSigningId, path: dmgFilePath, options: "--force")
|
||||
try await codesign(identity: packageSigningId, path: dmgFilePath, options: "--force")
|
||||
|
||||
if let appleId, let applePassword, let appleTeamId {
|
||||
print("Notarising DMG with Apple ID \(appleId)…")
|
||||
try notarisePackage(
|
||||
try await notarisePackage(
|
||||
packagePath: dmgFilePath,
|
||||
appleId: appleId,
|
||||
applePassword: applePassword,
|
||||
@ -152,11 +152,11 @@ func createDmgForAppBundle(
|
||||
|
||||
print("Creating Sparkle TBZ file…")
|
||||
let sparklePackagePath =
|
||||
try buildSparklePackage(packagePath: dmgFilePath, buildPath: buildPath)
|
||||
try await buildSparklePackage(packagePath: dmgFilePath, buildPath: buildPath)
|
||||
|
||||
if let sparklePackageSignKey {
|
||||
print("Signing Sparkle TBZ file…")
|
||||
try signSparklePackage(
|
||||
try await signSparklePackage(
|
||||
sparkleTbzPath: sparklePackagePath,
|
||||
buildPath: buildPath,
|
||||
signKey: sparklePackageSignKey
|
||||
|
@ -1,12 +1,21 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Nextcloud GmbH
|
||||
// SPDX-FileCopyrightText: 2024 Claudio Cambra
|
||||
// SPDX-FileCopyrightText: 2025 Iva Horn
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import Foundation
|
||||
|
||||
weak var globalTaskRef: Process?
|
||||
|
||||
///
|
||||
/// Run a shell command.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - launchPath: The command to run.
|
||||
/// - args: Arguments to pass.
|
||||
/// - env: Environment variables to pass for this specific command.
|
||||
/// - quiet: Whether the standard and error output of the command should be printed or omitted.
|
||||
///
|
||||
/// - Returns: Exit code of the command.
|
||||
///
|
||||
@discardableResult
|
||||
func run(
|
||||
_ launchPath: String,
|
||||
@ -14,11 +23,14 @@ func run(
|
||||
env: [String: String]? = nil,
|
||||
quiet: Bool = false,
|
||||
task: Process = Process()
|
||||
) -> Int32 {
|
||||
globalTaskRef = task
|
||||
) async -> Int32 {
|
||||
await State.shared.register(task)
|
||||
|
||||
signal(SIGINT) { _ in
|
||||
globalTaskRef?.terminate() // Send terminate signal to the task
|
||||
exit(0) // Exit the script after cleanup
|
||||
Task {
|
||||
await State.shared.terminate()
|
||||
exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
task.launchPath = launchPath
|
||||
@ -37,23 +49,48 @@ func run(
|
||||
|
||||
task.launch()
|
||||
task.waitUntilExit()
|
||||
|
||||
return task.terminationStatus
|
||||
}
|
||||
|
||||
///
|
||||
/// Run a shell command.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - launchPath: The command to run.
|
||||
/// - args: Arguments to pass.
|
||||
/// - env: Environment variables to pass for this specific command.
|
||||
/// - quiet: Whether the standard and error output of the command should be printed or omitted.
|
||||
///
|
||||
/// - Returns: Exit code of the command.
|
||||
///
|
||||
func run(
|
||||
_ launchPath: String,
|
||||
_ args: String...,
|
||||
env: [String: String]? = nil,
|
||||
quiet: Bool = false
|
||||
) -> Int32 {
|
||||
return run(launchPath, args, env: env, quiet: quiet)
|
||||
) async -> Int32 {
|
||||
return await run(launchPath, args, env: env, quiet: quiet)
|
||||
}
|
||||
|
||||
///
|
||||
/// Run multiple shell commands.
|
||||
///
|
||||
/// - Returns: Exit code of the command.
|
||||
///
|
||||
@discardableResult
|
||||
func shell(_ commands: String..., env: [String: String]? = nil, quiet: Bool = false) -> Int32 {
|
||||
return run("/bin/zsh", ["-c"] + commands, env: env, quiet: quiet)
|
||||
func shell(_ commands: String..., env: [String: String]? = nil, quiet: Bool = false) async -> Int32 {
|
||||
return await run("/bin/zsh", ["-c"] + commands, env: env, quiet: quiet)
|
||||
}
|
||||
|
||||
func commandExists(_ command: String) -> Bool {
|
||||
return run("/usr/bin/type", command, quiet: true) == 0
|
||||
///
|
||||
/// Check whether the given shell command is available in the shell.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - command: The command to check for availability.
|
||||
///
|
||||
/// - Returns: `true` in case of availability, otherwise `false`.
|
||||
///
|
||||
func commandExists(_ command: String) async -> Bool {
|
||||
return await run("/usr/bin/type", command, quiet: true) == 0
|
||||
}
|
||||
|
@ -1,7 +1,7 @@
|
||||
/*
|
||||
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: GPL-2.0-or-later
|
||||
*/
|
||||
// SPDX-FileCopyrightText: Nextcloud GmbH
|
||||
// SPDX-FileCopyrightText: 2024 Claudio Cambra
|
||||
// SPDX-FileCopyrightText: 2025 Iva Horn
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
import ArgumentParser
|
||||
import Foundation
|
||||
@ -106,24 +106,24 @@ struct Build: ParsableCommand {
|
||||
@Flag(help: "Build in developer mode.")
|
||||
var dev = false
|
||||
|
||||
mutating func run() throws {
|
||||
mutating func run() async throws {
|
||||
print("Configuring build tooling.")
|
||||
|
||||
if codeSignIdentity != nil {
|
||||
guard commandExists("codesign") else {
|
||||
guard await commandExists("codesign") else {
|
||||
throw MacCrafterError.environmentError("codesign not found, cannot proceed.")
|
||||
}
|
||||
}
|
||||
|
||||
try installIfMissing("git", "xcode-select --install")
|
||||
try installIfMissing(
|
||||
try await installIfMissing("git", "xcode-select --install")
|
||||
try await installIfMissing(
|
||||
"brew",
|
||||
"curl -fsSL \(brewInstallShUrl) | /bin/bash",
|
||||
installCommandEnv: ["NONINTERACTIVE": "1"]
|
||||
)
|
||||
try installIfMissing("wget", "brew install wget")
|
||||
try installIfMissing("inkscape", "brew install inkscape")
|
||||
try installIfMissing("python3", "brew install pyenv && pyenv install 3.12.4")
|
||||
try await installIfMissing("wget", "brew install wget")
|
||||
try await installIfMissing("inkscape", "brew install inkscape")
|
||||
try await installIfMissing("python3", "brew install pyenv && pyenv install 3.12.4")
|
||||
|
||||
print("Build tooling configured.")
|
||||
|
||||
@ -142,26 +142,26 @@ struct Build: ParsableCommand {
|
||||
print("KDE Craft is already cloned.")
|
||||
} else {
|
||||
print("Cloning KDE Craft...")
|
||||
guard shell("\(gitCloneCommand) \(craftMasterGitUrl) \(craftMasterDir)") == 0 else {
|
||||
guard await shell("\(gitCloneCommand) \(craftMasterGitUrl) \(craftMasterDir)") == 0 else {
|
||||
throw MacCrafterError.gitError("The referenced CraftMaster repository could not be cloned.")
|
||||
}
|
||||
}
|
||||
|
||||
print("Configuring required KDE Craft blueprint repositories...")
|
||||
guard shell("\(craftCommand) --add-blueprint-repository '\(kdeBlueprintsGitUrl)|\(kdeBlueprintsGitRef)|'") == 0 else {
|
||||
guard await shell("\(craftCommand) --add-blueprint-repository '\(kdeBlueprintsGitUrl)|\(kdeBlueprintsGitRef)|'") == 0 else {
|
||||
throw MacCrafterError.craftError("Error adding KDE blueprint repository.")
|
||||
}
|
||||
guard shell("\(craftCommand) --add-blueprint-repository '\(clientBlueprintsGitUrl)|\(clientBlueprintsGitRef)|'") == 0 else {
|
||||
guard await shell("\(craftCommand) --add-blueprint-repository '\(clientBlueprintsGitUrl)|\(clientBlueprintsGitRef)|'") == 0 else {
|
||||
throw MacCrafterError.craftError("Error adding Nextcloud Client blueprint repository.")
|
||||
}
|
||||
|
||||
print("Crafting KDE Craft...")
|
||||
guard shell("\(craftCommand) craft") == 0 else {
|
||||
guard await shell("\(craftCommand) craft") == 0 else {
|
||||
throw MacCrafterError.craftError("Error crafting KDE Craft.")
|
||||
}
|
||||
|
||||
print("Crafting Nextcloud Desktop Client dependencies...")
|
||||
guard shell("\(craftCommand) --install-deps \(craftBlueprintName)") == 0 else {
|
||||
guard await shell("\(craftCommand) --install-deps \(craftBlueprintName)") == 0 else {
|
||||
throw MacCrafterError.craftError("Error installing dependencies.")
|
||||
}
|
||||
}
|
||||
@ -187,15 +187,19 @@ struct Build: ParsableCommand {
|
||||
if !disableAutoUpdater {
|
||||
print("Configuring Sparkle auto-updater.")
|
||||
|
||||
let sparkleDownloadResult = await shell("wget \(sparkleDownloadUrl) -O \(buildPath)/Sparkle.tar.xz")
|
||||
|
||||
let fm = FileManager.default
|
||||
guard fm.fileExists(atPath: "\(buildPath)/Sparkle.tar.xz") ||
|
||||
shell("wget \(sparkleDownloadUrl) -O \(buildPath)/Sparkle.tar.xz") == 0
|
||||
sparkleDownloadResult == 0
|
||||
else {
|
||||
throw MacCrafterError.environmentError("Error downloading sparkle.")
|
||||
}
|
||||
|
||||
let sparkleUnarchiveResult = await shell("tar -xvf \(buildPath)/Sparkle.tar.xz -C \(buildPath)")
|
||||
|
||||
guard fm.fileExists(atPath: "\(buildPath)/Sparkle.framework") ||
|
||||
shell("tar -xvf \(buildPath)/Sparkle.tar.xz -C \(buildPath)") == 0
|
||||
sparkleUnarchiveResult == 0
|
||||
else {
|
||||
throw MacCrafterError.environmentError("Error unpacking sparkle.")
|
||||
}
|
||||
@ -232,7 +236,7 @@ struct Build: ParsableCommand {
|
||||
let buildMode = fullRebuild ? "-i" : disableAppBundle ? "compile" : "--compile --install"
|
||||
let offlineMode = offline ? "--offline" : ""
|
||||
let allOptionsString = craftOptions.map({ "--options \"\($0)\"" }).joined(separator: " ")
|
||||
guard shell(
|
||||
guard await shell(
|
||||
"\(craftCommand) --buildtype \(buildType) \(buildMode) \(offlineMode) \(allOptionsString) \(craftBlueprintName)"
|
||||
) == 0 else {
|
||||
// Troubleshooting: This can happen because a CraftMaster repository was cloned which does not contain the commit defined in craftmaster.ini of this project due to use of customized forks.
|
||||
@ -243,7 +247,7 @@ struct Build: ParsableCommand {
|
||||
if let codeSignIdentity {
|
||||
print("Code-signing Nextcloud Desktop Client libraries and frameworks...")
|
||||
let entitlementsPath = "\(clientBuildDir)/work/build/admin/osx/macosx.entitlements"
|
||||
try codesignClientAppBundle(
|
||||
try await codesignClientAppBundle(
|
||||
at: clientAppDir,
|
||||
withCodeSignIdentity: codeSignIdentity,
|
||||
usingEntitlements: entitlementsPath
|
||||
@ -262,7 +266,7 @@ struct Build: ParsableCommand {
|
||||
try fm.copyItem(atPath: clientAppDir, toPath: "\(productPath)/\(appName).app")
|
||||
|
||||
if package {
|
||||
try packageAppBundle(
|
||||
try await packageAppBundle(
|
||||
productPath: productPath,
|
||||
buildPath: buildPath,
|
||||
craftTarget: craftTarget,
|
||||
@ -292,12 +296,12 @@ struct Codesign: ParsableCommand {
|
||||
@Option(name: [.short, .long], help: "Entitlements to apply to the app bundle.")
|
||||
var entitlementsPath: String?
|
||||
|
||||
mutating func run() throws {
|
||||
mutating func run() async throws {
|
||||
let absolutePath = appBundlePath.hasPrefix("/")
|
||||
? appBundlePath
|
||||
: "\(FileManager.default.currentDirectoryPath)/\(appBundlePath)"
|
||||
|
||||
try codesignClientAppBundle(
|
||||
try await codesignClientAppBundle(
|
||||
at: absolutePath,
|
||||
withCodeSignIdentity: codeSignIdentity,
|
||||
usingEntitlements: entitlementsPath
|
||||
@ -335,9 +339,9 @@ struct CreateDMG: ParsableCommand {
|
||||
@Option(name: [.long], help: "Sparkle package signing key.")
|
||||
var sparklePackageSignKey: String?
|
||||
|
||||
mutating func run() throws {
|
||||
try installIfMissing("create-dmg", "brew install create-dmg")
|
||||
try createDmgForAppBundle(
|
||||
mutating func run() async throws {
|
||||
try await installIfMissing("create-dmg", "brew install create-dmg")
|
||||
try await createDmgForAppBundle(
|
||||
appBundlePath: appBundlePath,
|
||||
productPath: productPath,
|
||||
buildPath: buildPath,
|
||||
@ -384,8 +388,8 @@ struct Package: ParsableCommand {
|
||||
@Option(name: [.long], help: "Sparkle package signing key.")
|
||||
var sparklePackageSignKey: String?
|
||||
|
||||
mutating func run() throws {
|
||||
try packageAppBundle(
|
||||
mutating func run() async throws {
|
||||
try await packageAppBundle(
|
||||
productPath: productPath,
|
||||
buildPath: buildPath,
|
||||
craftTarget: archToCraftTarget(arch),
|
||||
|
Reference in New Issue
Block a user