Skip to content

Commit 25d8a61

Browse files
committed
feat(mac-crafter): New logging.
- Write to standard output in terminal environment. - Write to unified logging system. - Write to log file. - All simultaneously. - Standard output and errors from child processes launched are captured, too. Signed-off-by: Iva Horn <[email protected]>
1 parent e1bbb70 commit 25d8a61

File tree

7 files changed

+180
-39
lines changed

7 files changed

+180
-39
lines changed

admin/osx/mac-crafter/README.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
- SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
33
- SPDX-License-Identifier: GPL-2.0-or-later
44
-->
5-
# Mac Crafter
5+
# mac-crafter
66

77
mac-crafter is a tool to easily build a fully functional Nextcloud Desktop Client for macOS.
88
It automates cloning, configuring, crafting, codesigning, packaging, and even disk image creation of the client.
@@ -58,6 +58,12 @@ Additional preparation is necessary, though.
5858
6. Navigate to the "Options" tab.
5959
7. Enable and define a custom working directory. The root of this Swift package, to be specific.
6060

61+
## Troubleshooting
62+
63+
mac-crafter has its own simple logging facility which writes to standard output in a terminal environment, the unified logging system of macOS and a log file simultaneously.
64+
You can stream the log messages in the Console app of macOS by searching for the "mac-crafter" process and "Log" subsystem.
65+
Alternatively, you can follow the latest log file created in `~/Library/Logs/mac-crafter`.
66+
6167
## License
6268

6369
Distributed under the terms of the GPL-2.0-or-later license.

admin/osx/mac-crafter/Sources/Commands/Build.swift

Lines changed: 19 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ struct Build: AsyncParsableCommand {
123123
mutating func run() async throws {
124124
let stopwatch = Stopwatch()
125125

126-
print("Ensuring build dependencies are met...")
126+
Log.info("Ensuring build dependencies are met...")
127127
stopwatch.record("Build Dependencies")
128128

129129
if codeSignIdentity != nil {
@@ -142,7 +142,7 @@ struct Build: AsyncParsableCommand {
142142
try await installIfMissing("inkscape", "brew install inkscape")
143143
try await installIfMissing("python3", "brew install pyenv && pyenv install 3.12.4")
144144

145-
print("Build dependencies are installed.")
145+
Log.info("Build dependencies are installed.")
146146

147147
let fm = FileManager.default
148148
let buildURL = URL(fileURLWithPath: buildPath).standardized
@@ -157,15 +157,15 @@ struct Build: AsyncParsableCommand {
157157
stopwatch.record("KDE Craft Setup")
158158

159159
if fm.fileExists(atPath: craftMasterDir.path) {
160-
print("KDE Craft is already cloned.")
160+
Log.info("KDE Craft is already cloned.")
161161
} else {
162-
print("Cloning KDE Craft...")
162+
Log.info("Cloning KDE Craft...")
163163
guard await shell("\(gitCloneCommand) \(craftMasterGitUrl) \(craftMasterDir.path)") == 0 else {
164164
throw MacCrafterError.gitError("The referenced CraftMaster repository could not be cloned from \(craftMasterGitUrl) to \(craftMasterDir.path)")
165165
}
166166
}
167167

168-
print("Configuring required KDE Craft blueprint repositories...")
168+
Log.info("Configuring required KDE Craft blueprint repositories...")
169169
stopwatch.record("Craft Blueprints Configuration")
170170

171171
guard await shell("\(craftCommand) --add-blueprint-repository '\(kdeBlueprintsGitUrl)|\(kdeBlueprintsGitRef)|'") == 0 else {
@@ -176,21 +176,21 @@ struct Build: AsyncParsableCommand {
176176
throw MacCrafterError.craftError("Error adding Nextcloud Client blueprint repository.")
177177
}
178178

179-
print("Crafting KDE Craft...")
179+
Log.info("Crafting KDE Craft...")
180180
stopwatch.record("Craft Crafting")
181181

182182
guard await shell("\(craftCommand) craft") == 0 else {
183183
throw MacCrafterError.craftError("Error crafting KDE Craft.")
184184
}
185185

186-
print("Crafting Nextcloud Desktop Client dependencies...")
186+
Log.info("Crafting Nextcloud Desktop Client dependencies...")
187187
stopwatch.record("Nextcloud Client Dependencies Crafting")
188188

189189
guard await shell("\(craftCommand) --install-deps \(craftBlueprintName)") == 0 else {
190190
throw MacCrafterError.craftError("Error installing dependencies.")
191191
}
192192
} else {
193-
print("Skipping KDE Craft configuration because it is already and no reconfiguration was requested.")
193+
Log.info("Skipping KDE Craft configuration because it is already and no reconfiguration was requested.")
194194
}
195195

196196
var craftOptions = [
@@ -212,7 +212,7 @@ struct Build: AsyncParsableCommand {
212212
}
213213

214214
if disableAutoUpdater == false {
215-
print("Configuring Sparkle auto-updater.")
215+
Log.info("Configuring Sparkle auto-updater.")
216216

217217
stopwatch.record("Sparke Configuration")
218218

@@ -233,12 +233,12 @@ struct Build: AsyncParsableCommand {
233233
.appendingPathComponent("build")
234234
.appendingPathComponent(craftBlueprintName)
235235

236-
print("Crafting \(appName) Desktop Client...")
236+
Log.info("Crafting \(appName) Desktop Client...")
237237
stopwatch.record("Desktop Client Crafting")
238238

239239
if fullRebuild {
240240
if fm.fileExists(atPath: clientBuildURL.path) {
241-
print("Removing existing client build directory at: \(clientBuildURL.path)")
241+
Log.info("Removing existing client build directory at: \(clientBuildURL.path)")
242242

243243
do {
244244
try fm.removeItem(atPath: clientBuildURL.path)
@@ -258,11 +258,11 @@ struct Build: AsyncParsableCommand {
258258
.appendingPathComponent("MacOSX")
259259

260260
if fm.fileExists(atPath: shellIntegrationURL.path) {
261-
print("Removing existing shell integration build artifacts...")
261+
Log.info("Removing existing shell integration build artifacts...")
262262
do {
263263
try fm.removeItem(atPath: shellIntegrationURL.path)
264264
} catch let error {
265-
print("ERROR: Error removing shell integration build directory: \(error)")
265+
Log.error("Failed to remove shell integration build directory: \(error)")
266266
throw MacCrafterError.craftError("Failed to remove existing shell integration build directory!")
267267
}
268268
}
@@ -282,7 +282,7 @@ struct Build: AsyncParsableCommand {
282282
.appendingPathComponent("\(appName).app")
283283

284284
if let codeSignIdentity {
285-
print("Signing Nextcloud Desktop Client libraries and frameworks...")
285+
Log.info("Signing Nextcloud Desktop Client libraries and frameworks...")
286286
stopwatch.record("Code Signing")
287287

288288
let appEntitlements = clientBuildURL
@@ -307,16 +307,16 @@ struct Build: AsyncParsableCommand {
307307

308308
for file in entitlements.values {
309309
if FileManager.default.fileExists(atPath: file.path) {
310-
print("Using entitlement manifest: \(file.path)")
310+
Log.info("Using entitlement manifest: \(file.path)")
311311
} else {
312-
print("ERROR: Entitlement manifest does not exist: \(file.path)")
312+
Log.error("Entitlement manifest does not exist: \(file.path)")
313313
}
314314
}
315315

316316
try await Signer.signMainBundle(at: clientAppURL, codeSignIdentity: codeSignIdentity, entitlements: entitlements)
317317
}
318318

319-
print("Placing Nextcloud Desktop Client in \(productPath)...")
319+
Log.info("Placing Nextcloud Desktop Client in \(productPath)...")
320320

321321
if !fm.fileExists(atPath: productPath) {
322322
try fm.createDirectory(atPath: productPath, withIntermediateDirectories: true, attributes: nil)
@@ -345,7 +345,7 @@ struct Build: AsyncParsableCommand {
345345
)
346346
}
347347

348-
print("Done!")
349-
print(stopwatch.report())
348+
Log.info("Done!")
349+
Log.info(stopwatch.report())
350350
}
351351
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
// SPDX-FileCopyrightText: Nextcloud GmbH
2+
// SPDX-FileCopyrightText: 2026 Iva Horn
3+
// SPDX-License-Identifier: GPL-2.0-or-later
4+
5+
import Foundation
6+
import os
7+
8+
///
9+
/// A simple logging facility for Mac Crafter to abstract terminal output, log file and unified logging system.
10+
///
11+
actor Log: Sendable {
12+
private static let shared = Log()
13+
private let handle: FileHandle?
14+
private let logger: Logger
15+
16+
init() {
17+
let manager = FileManager.default
18+
19+
let directory = try! manager
20+
.url(for: .libraryDirectory, in: .userDomainMask, appropriateFor: nil, create: true)
21+
.appendingPathComponent("Logs")
22+
.appendingPathComponent("mac-crafter")
23+
24+
if manager.fileExists(atPath: directory.path) == false {
25+
try! manager.createDirectory(at: directory, withIntermediateDirectories: true)
26+
}
27+
28+
let file = directory
29+
.appendingPathComponent(UUID().uuidString)
30+
.appendingPathExtension("log")
31+
32+
manager.createFile(atPath: file.path, contents: nil)
33+
34+
print("Writing log file: \(file.path)")
35+
handle = try? FileHandle(forWritingTo: file)
36+
logger = Logger(subsystem: "Log", category: "")
37+
logger.info("Writing log file: \(file.path)")
38+
}
39+
40+
private func log(level: OSLogType, message: String) {
41+
guard message.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty == false else {
42+
return
43+
}
44+
45+
print(message)
46+
logger.log(level: level, "\(message, privacy: .public)")
47+
48+
guard let handle else {
49+
return
50+
}
51+
52+
guard let data = "\(message)\n".data(using: .utf8) else {
53+
return
54+
}
55+
56+
try? handle.write(contentsOf: data)
57+
}
58+
59+
///
60+
/// Write an informative message.
61+
///
62+
static func info(_ message: String) {
63+
Task {
64+
await shared.log(level: .info, message: message)
65+
}
66+
}
67+
68+
///
69+
/// Write an error message.
70+
///
71+
static func error(_ message: String) {
72+
Task {
73+
await shared.log(level: .error, message: message)
74+
}
75+
}
76+
}

admin/osx/mac-crafter/Sources/Utils/Install.swift

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,12 +29,12 @@ func installIfMissing(
2929
installCommandEnv: [String: String]? = nil
3030
) async throws {
3131
if await commandExists(command) {
32-
print("Required command \"\(command)\" already is installed.")
32+
Log.info("Required command \"\(command)\" already is installed.")
3333
} else {
34-
print("Required command \"\(command)\" is missing, installing...")
34+
Log.info("Required command \"\(command)\" is missing, installing...")
3535
guard await shell(installCommand, env: installCommandEnv) == 0 else {
3636
throw InstallError.failedToInstall("Failed to install \"\(command)\"!")
3737
}
38-
print("\"\(command)\" installed.")
38+
Log.info("\"\(command)\" installed.")
3939
}
4040
}

admin/osx/mac-crafter/Sources/Utils/Packaging.swift

Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,15 +24,15 @@ fileprivate func buildPackage(appName: String, buildWorkPath: String, productPat
2424
throw PackagingError.projectNameSettingError("Could not set project name in pkgproj!")
2525
}
2626
guard await shell("packagesbuild -v --build-folder \(productPath) -F \(productPath) \(pkgprojPath)") == 0 else {
27-
throw PackagingError.packageBuildError("Error building pkg file!")
27+
throw PackagingError.packageBuildError("Error building package file!")
2828
}
2929
return "\(productPath)/\(packageFile)"
3030
}
3131

3232
fileprivate func signPackage(packagePath: String, packageSigningId: String) async throws {
3333
let packagePathNew = "\(packagePath).new"
3434
guard await shell("productsign --timestamp --sign '\(packageSigningId)' \(packagePath) \(packagePathNew)") == 0 else {
35-
throw PackagingError.packageSigningError("Could not sign pkg file!")
35+
throw PackagingError.packageSigningError("Could not sign package file!")
3636
}
3737
let fm = FileManager.default
3838
try fm.removeItem(atPath: packagePath)
@@ -76,7 +76,7 @@ func packageAppBundle(
7676
appleTeamId: String?,
7777
sparklePackageSignKey: String?
7878
) async throws {
79-
print("Creating pkg file for client…")
79+
Log.info("Creating package file for client…")
8080
let buildWorkPath = "\(buildPath)/\(craftTarget)/build/\(craftBlueprintName)/work/build"
8181
let packagePath = try await buildPackage(
8282
appName: appName,
@@ -85,11 +85,11 @@ func packageAppBundle(
8585
)
8686

8787
if let packageSigningId {
88-
print("Signing pkg with \(packageSigningId)")
88+
Log.info("Signing package with \(packageSigningId)")
8989
try await signPackage(packagePath: packagePath, packageSigningId: packageSigningId)
9090

9191
if let appleId, let applePassword, let appleTeamId {
92-
print("Notarising pkg with Apple ID \(appleId)")
92+
Log.info("Notarising package with Apple ID \(appleId)")
9393
try await notarisePackage(
9494
packagePath: packagePath,
9595
appleId: appleId,
@@ -99,12 +99,12 @@ func packageAppBundle(
9999
}
100100
}
101101

102-
print("Creating Sparkle TBZ file…")
102+
Log.info("Creating Sparkle TBZ file…")
103103
let sparklePackagePath =
104104
try await buildSparklePackage(packagePath: packagePath, buildPath: buildPath)
105105

106106
if let sparklePackageSignKey {
107-
print("Signing Sparkle TBZ file…")
107+
Log.info("Signing Sparkle TBZ file…")
108108
try await signSparklePackage(
109109
sparkleTbzPath: sparklePackagePath,
110110
buildPath: buildPath,
@@ -124,7 +124,7 @@ func createDmgForAppBundle(
124124
appleTeamId: String?,
125125
sparklePackageSignKey: String?
126126
) async throws {
127-
print("Creating disk image for the client…")
127+
Log.info("Creating disk image for the client…")
128128

129129
let dmgFilePath = URL(fileURLWithPath: productPath)
130130
.appendingPathComponent(appName)
@@ -136,11 +136,11 @@ func createDmgForAppBundle(
136136
}
137137

138138
if let packageSigningId {
139-
print("Signing DMG with \(packageSigningId)")
139+
Log.info("Signing disk image with \(packageSigningId)")
140140
await Signer.sign(at: URL(fileURLWithPath: dmgFilePath), with: packageSigningId, entitlements: nil)
141141

142142
if let appleId, let applePassword, let appleTeamId {
143-
print("Notarising DMG with Apple ID \(appleId)")
143+
Log.info("Notarising disk image with Apple ID \(appleId)")
144144

145145
try await notarisePackage(
146146
packagePath: dmgFilePath,
@@ -151,11 +151,11 @@ func createDmgForAppBundle(
151151
}
152152
}
153153

154-
print("Creating Sparkle TBZ file…")
154+
Log.info("Creating Sparkle TBZ file…")
155155
let sparklePackagePath = try await buildSparklePackage(packagePath: dmgFilePath, buildPath: buildPath)
156156

157157
if let sparklePackageSignKey {
158-
print("Signing Sparkle TBZ file…")
158+
Log.info("Signing Sparkle TBZ file…")
159159

160160
try await signSparklePackage(
161161
sparkleTbzPath: sparklePackagePath,

0 commit comments

Comments
 (0)