Skip to content

Commit 176c5f0

Browse files
committed
Work in progress: Entitlements through CMake.
- Rebased branch onto i2h3/proper-macos-sandboxing - Removed extension entitlement source files and related build settings from the Xcode project to be build with CMake instead. - Set up CMake to generate the required entitlement manifests by itself to be used later on by Mac Crafter in the code signing. - Updated the mac-crafter build subcommand to rely on URLs instead of path strings. - Updated the mac-crafter build subcommand to reference the entitlement manifests generated by CMake. - Updated the mac-crafter codesign subcommand to require arguments for specifying the entitlement manifests to sign the app extensions. - Updated README of mac-crafter and removed a lot of outdated and redundant information. Signed-off-by: Iva Horn <[email protected]>
1 parent d7ffb9d commit 176c5f0

File tree

9 files changed

+115
-219
lines changed

9 files changed

+115
-219
lines changed

admin/osx/mac-crafter/README.md

Lines changed: 6 additions & 137 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
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.
8-
It automates cloning, configuring, crafting, codesigning, packaging, and even DMG creation of the client.
9-
The tool is built with Swift’s ArgumentParser and it drives the KDE Craft build system along with some Python scripts and shell commands.
8+
It automates cloning, configuring, crafting, codesigning, packaging, and even disk image creation of the client.
9+
The tool is built with Apple’s ArgumentParser and it drives the KDE Craft build system along with some Python scripts and shell commands.
1010

1111
## System Requirements
1212

13-
- macOS 11 Big Sur or newer
13+
- macOS 12 Monterey or newer
1414
- Xcode
1515
- Python3
1616
- Homebrew (for installing additional tools like `inkscape`, `pyenv`, and `create-dmg`)
@@ -28,139 +28,8 @@ The script will also clone the KDE Craft repository if it is not already present
2828

2929
## Usage
3030

31-
mac-crafter comes with several subcommands:
32-
33-
### Build
34-
35-
This is the default command and it handles:
36-
- Configuring and/or cloning KDE Craft (using the CraftMaster repository)
37-
- Adding the Nextcloud Desktop Client blueprints
38-
- Crafting KDE Craft projects and installing dependencies
39-
- Building the client with options for a full rebuild, offline mode, and more
40-
41-
**Usage Example:**
42-
43-
```
44-
swift run mac-crafter [options]
45-
```
46-
47-
**Common Options:**
48-
49-
- **Repository and Build Paths:**
50-
- `--repo-root-dir`: Path to the Nextcloud Desktop Client git repository (default is `../../../` relative to the current directory).
51-
- `--build-path`: Directory where build files are written.
52-
- `--product-path`: Directory where the final product (app bundle) will be placed.
53-
54-
- **Build Settings:**
55-
- `--arch`: Architecture to build for (e.g. `arm64`, `x86_64`).
56-
- `--build-type`: Build type (e.g. `Release`, `RelWithDebInfo`, `Debug`).
57-
- `--craft-blueprint-name`: Blueprint name for Nextcloud Desktop Client (default is `"nextcloud-client"`).
58-
- `--full-rebuild`: Forces a full rebuild by wiping existing build artifacts.
59-
- `--offline`: Run the build offline (do not update craft).
60-
61-
- **Code Signing & Notarisation:**
62-
- `--code-sign-identity (-c)`: Code signing identity for the client and libraries.
63-
- `--apple-id`, `--apple-password`, `--apple-team-id`: Credentials for notarisation.
64-
- `--package-signing-id`: Identifier used for package signing.
65-
66-
- **Advanced Options:**
67-
- `--disable-autoupdater`: Build without the Sparkle auto-updater.
68-
- `--build-tests`: Optionally build the test suite.
69-
- `--build-file-provider-module`: Build the File Provider Module.
70-
- `--dev`: Build in developer mode which, for example, appends "Dev" to the app name and sets a dev flag in the craft options.
71-
- `--override-server-url` and `--force-override-server-url`: Override server URL settings for the client.
72-
73-
The build process automatically ensures necessary tools (like git, inkscape, python3) are installed—invoking installation commands on missing dependencies.
74-
75-
### Codesign
76-
77-
Use this subcommand to codesign an existing Nextcloud Desktop Client app bundle.
78-
79-
**Usage Example:**
80-
81-
```
82-
swift run mac-crafter codesign -c "Apple Development: <certificate common name>" <path-to-app-bundle>
83-
```
84-
85-
- **Options:**
86-
- `appBundlePath`: Path to the app bundle.
87-
- `--code-sign-identity (-c)`: Code signing identity to use.
88-
89-
### Package
90-
91-
This command is used to package the client after building. It prepares the app bundle and can also perform package signing and notarisation.
92-
93-
**Usage Example:**
94-
95-
```
96-
swift run mac-crafter package [options]
97-
```
98-
99-
- **Options:**
100-
- `--arch`: Target architecture.
101-
- `--build-path`, `--product-path`: Build and product directories.
102-
- `--craft-blueprint-name`: Blueprint name.
103-
- `--app-name`: The branded name of the application.
104-
- Various notarisation options (`--apple-id`, `--apple-password`, `--apple-team-id`).
105-
- Signing options such as `--package-signing-id` and `--sparkle-package-sign-key`.
106-
107-
### CreateDMG
108-
109-
This subcommand creates a DMG (disk image) for the client app bundle.
110-
111-
**Usage Example:**
112-
113-
```
114-
swift run mac-crafter createDMG <path-to-app-bundle> [options]
115-
```
116-
117-
- **Options:**
118-
- `appBundlePath`: The app bundle’s path.
119-
- `--product-path`: Where the final DMG and product will be placed.
120-
- `--build-path`: Directory for temporary build files.
121-
- `--app-name`: Application's name.
122-
- Notarisation and signing options similar to the Package command.
123-
124-
## How It Works
125-
126-
1. **Tooling Configuration:**
127-
The build command checks for necessary tools (like `codesign`, `git`, `brew`, `inkscape`, and `python3`) and auto-installs missing dependencies if needed.
128-
2. **KDE Craft Configuration:**
129-
- If KDE Craft isn’t already cloned or if a reconfiguration is triggered, the tool clones the CraftMaster repository and configures it using a provided INI file.
130-
- Next, it adds the Nextcloud Desktop Client blueprints, then crafts KDE Craft and installs the required dependencies.
131-
3. **Craft Options Setup:**
132-
The build process assembles a set of options including source directory, architecture, build tests, auto-updater settings, and more.
133-
4. **Building, Codesigning, and Packaging:**
134-
The tool then builds the client, optionally performs a full rebuild, and if a codesign identity is provided, signs the final app bundle. Finally, it copies the finished app bundle to the product directory.
135-
5. **Optional DMG Creation:**
136-
Use the CreateDMG subcommand to bundle the built client into a DMG for distribution.
137-
138-
## Quick Start
139-
140-
For a basic build and codesigning:
141-
```
142-
swift run mac-crafter -c "Apple Development: MyCertificate"
143-
```
144-
145-
For a full rebuild on a specific architecture:
146-
```
147-
swift run mac-crafter --arch arm64 --full-rebuild -c "Apple Development: MyCertificate"
148-
```
149-
150-
To package the app:
151-
```
152-
swift run mac-crafter package -c "Apple Development: MyCertificate" --arch arm64
153-
```
154-
155-
To create a DMG:
156-
```
157-
swift run mac-crafter createDMG /path/to/Nextcloud.app --app-name Nextcloud
158-
```
159-
160-
For more details on all available options, run:
161-
```
162-
swift run mac-crafter --help
163-
```
31+
mac-crafter comes with several subcommands.
32+
To see a full reference, run `mac-crafter --help` or `mac-crafter <subcommand> --help` for further specific information about the command.
16433

16534
## Additional Information
16635

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

Lines changed: 70 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -145,20 +145,18 @@ struct Build: AsyncParsableCommand {
145145
print("Build dependencies are installed.")
146146

147147
let fm = FileManager.default
148-
let craftMasterDir = "\(buildPath)/craftmaster"
149-
let craftMasterIni = "\(repoRootDir)/craftmaster.ini"
150-
let craftMasterPy = "\(craftMasterDir)/CraftMaster.py"
148+
let buildURL = URL(fileURLWithPath: buildPath).standardized
149+
let repoRootURL = URL(fileURLWithPath: repoRootDir).standardized
150+
let craftMasterDir = buildURL.appendingPathComponent("craftmaster")
151+
let craftMasterIni = repoRootURL.appendingPathComponent("craftmaster.ini")
152+
let craftMasterPy = craftMasterDir.appendingPathComponent("CraftMaster.py")
151153
let craftTarget = archToCraftTarget(arch)
152-
let craftCommand =
153-
"python3 \(craftMasterPy) --config \(craftMasterIni) --target \(craftTarget) -c"
154-
155-
if !fm.fileExists(atPath: craftMasterDir) || reconfigureCraft {
156-
print("Configuring KDE Craft.")
157-
158-
print("Configuring KDE Craft...")
154+
let craftCommand = "python3 \(craftMasterPy.path) --config \(craftMasterIni.path) --target \(craftTarget) -c"
155+
156+
if !fm.fileExists(atPath: craftMasterDir.path) || reconfigureCraft {
159157
stopwatch.record("KDE Craft Setup")
160158

161-
if fm.fileExists(atPath: craftMasterDir) {
159+
if fm.fileExists(atPath: craftMasterDir.path) {
162160
print("KDE Craft is already cloned.")
163161
} else {
164162
print("Cloning KDE Craft...")
@@ -168,36 +166,35 @@ struct Build: AsyncParsableCommand {
168166
}
169167

170168
print("Configuring required KDE Craft blueprint repositories...")
171-
guard await shell("\(craftCommand) --add-blueprint-repository '\(kdeBlueprintsGitUrl)|\(kdeBlueprintsGitRef)|'") == 0 else {
172169
stopwatch.record("Craft Blueprints Configuration")
173170

171+
guard await shell("\(craftCommand) --add-blueprint-repository '\(kdeBlueprintsGitUrl)|\(kdeBlueprintsGitRef)|'") == 0 else {
174172
throw MacCrafterError.craftError("Error adding KDE blueprint repository.")
175173
}
176-
guard await shell("\(craftCommand) --add-blueprint-repository '\(clientBlueprintsGitUrl)|\(clientBlueprintsGitRef)|'") == 0 else {
177174

175+
guard await shell("\(craftCommand) --add-blueprint-repository '\(clientBlueprintsGitUrl)|\(clientBlueprintsGitRef)|'") == 0 else {
178176
throw MacCrafterError.craftError("Error adding Nextcloud Client blueprint repository.")
179177
}
180178

181179
print("Crafting KDE Craft...")
182-
guard await shell("\(craftCommand) craft") == 0 else {
183180
stopwatch.record("Craft Crafting")
184181

182+
guard await shell("\(craftCommand) craft") == 0 else {
185183
throw MacCrafterError.craftError("Error crafting KDE Craft.")
186184
}
187185

188186
print("Crafting Nextcloud Desktop Client dependencies...")
189-
guard await shell("\(craftCommand) --install-deps \(craftBlueprintName)") == 0 else {
190187
stopwatch.record("Nextcloud Client Dependencies Crafting")
191188

189+
guard await shell("\(craftCommand) --install-deps \(craftBlueprintName)") == 0 else {
192190
throw MacCrafterError.craftError("Error installing dependencies.")
193191
}
194192
} else {
195193
print("Skipping KDE Craft configuration because it is already and no reconfiguration was requested.")
196194
}
197-
198195

199196
var craftOptions = [
200-
"\(craftBlueprintName).srcDir=\(repoRootDir)",
197+
"\(craftBlueprintName).srcDir=\(repoRootURL.path)",
201198
"\(craftBlueprintName).osxArchs=\(arch)",
202199
"\(craftBlueprintName).buildTests=\(buildTests ? "True" : "False")",
203200
"\(craftBlueprintName).buildMacOSBundle=\(disableAppBundle ? "False" : "True")",
@@ -231,28 +228,39 @@ struct Build: AsyncParsableCommand {
231228
craftOptions.append("\(craftBlueprintName).sparkleLibPath=\(buildPath)/Sparkle.framework")
232229
}
233230

234-
let clientBuildDir = "\(buildPath)/\(craftTarget)/build/\(craftBlueprintName)"
231+
let clientBuildURL = buildURL
232+
.appendingPathComponent(craftTarget)
233+
.appendingPathComponent("build")
234+
.appendingPathComponent(craftBlueprintName)
235+
235236
print("Crafting \(appName) Desktop Client...")
236237
stopwatch.record("Desktop Client Crafting")
237238

238239
if fullRebuild {
239240
do {
240-
try fm.removeItem(atPath: clientBuildDir)
241+
try fm.removeItem(atPath: clientBuildURL.path)
241242
} catch let error {
242-
print("WARNING! Error removing build directory: \(error)")
243+
print("ERROR: Error removing build directory: \(error)")
244+
throw MacCrafterError.craftError("Failed to remove existing build directory!")
243245
}
244246
} else {
245247
// HACK: When building the client we often run into issues with the shell integration
246248
// component -- particularly the FileProviderExt part. So we wipe out the build
247249
// artifacts so this part gets build first. Let's first check if we have an existing
248250
// build in the folder we expect
249-
let shellIntegrationDir = "\(clientBuildDir)/work/build/shell_integration/MacOSX"
250-
if fm.fileExists(atPath: shellIntegrationDir) {
251+
let shellIntegrationURL = clientBuildURL
252+
.appendingPathComponent("work")
253+
.appendingPathComponent("build")
254+
.appendingPathComponent("shell_integration")
255+
.appendingPathComponent("MacOSX")
256+
257+
if fm.fileExists(atPath: shellIntegrationURL.path) {
251258
print("Removing existing shell integration build artifacts...")
252259
do {
253-
try fm.removeItem(atPath: shellIntegrationDir)
260+
try fm.removeItem(atPath: shellIntegrationURL.path)
254261
} catch let error {
255-
print("WARNING! Error removing shell integration build directory: \(error)")
262+
print("ERROR: Error removing shell integration build directory: \(error)")
263+
throw MacCrafterError.craftError("Failed to remove existing shell integration build directory!")
256264
}
257265
}
258266
}
@@ -261,35 +269,62 @@ struct Build: AsyncParsableCommand {
261269
let offlineMode = offline ? "--offline" : ""
262270
let allOptionsString = craftOptions.map({ "--options \"\($0)\"" }).joined(separator: " ")
263271

264-
guard await shell(
265-
"\(craftCommand) --buildtype \(buildType) \(buildMode) \(offlineMode) \(allOptionsString) \(craftBlueprintName)"
266-
) == 0 else {
272+
guard await shell("\(craftCommand) --buildtype \(buildType) \(buildMode) \(offlineMode) \(allOptionsString) \(craftBlueprintName)") == 0 else {
267273
// 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.
268274
throw MacCrafterError.craftError("Error crafting Nextcloud Desktop Client.")
269275
}
270276

271-
let clientAppDir = "\(clientBuildDir)/image-\(buildType)-master/\(appName).app"
277+
let clientAppURL = clientBuildURL
278+
.appendingPathComponent("image-\(buildType)-master")
279+
.appendingPathComponent("\(appName).app")
272280

273281
if let codeSignIdentity {
274-
print("Code-signing Nextcloud Desktop Client libraries and frameworks...")
282+
print("Signing Nextcloud Desktop Client libraries and frameworks...")
275283
stopwatch.record("Code Signing")
276284

277-
try await Signer.signMainBundle(at: URL(fileURLWithPath: clientAppDir), codeSignIdentity: codeSignIdentity, developerBuild: dev)
285+
let appEntitlements = clientBuildURL
286+
.appendingPathComponent("work")
287+
.appendingPathComponent("build")
288+
.appendingPathComponent("admin")
289+
.appendingPathComponent("osx")
290+
.appendingPathComponent("macosx.entitlements")
291+
292+
let entitlementsDirectory = clientBuildURL
293+
.appendingPathComponent("work")
294+
.appendingPathComponent("build")
295+
.appendingPathComponent("shell_integration")
296+
.appendingPathComponent("MacOSX")
297+
298+
let entitlements: [String: URL] = [
299+
"\(appName).app": appEntitlements,
300+
"FileProviderExt.appex": entitlementsDirectory.appendingPathComponent("FileProviderExt.entitlements"),
301+
"FileProviderUIExt.appex": entitlementsDirectory.appendingPathComponent("FileProviderUIExt.entitlements"),
302+
"FinderSyncExt.appex": entitlementsDirectory.appendingPathComponent("FinderSyncExt.entitlements"),
303+
]
304+
305+
for file in entitlements.values {
306+
if FileManager.default.fileExists(atPath: file.path) {
307+
print("Using entitlement manifest: \(file.path)")
308+
} else {
309+
print("ERROR: Entitlement manifest does not exist: \(file.path)")
310+
}
311+
}
312+
313+
try await Signer.signMainBundle(at: clientAppURL, codeSignIdentity: codeSignIdentity, entitlements: entitlements, developerBuild: dev)
278314
}
279315

280316
print("Placing Nextcloud Desktop Client in \(productPath)...")
281317

282318
if !fm.fileExists(atPath: productPath) {
283-
try fm.createDirectory(
284-
atPath: productPath, withIntermediateDirectories: true, attributes: nil
285-
)
319+
try fm.createDirectory(atPath: productPath, withIntermediateDirectories: true, attributes: nil)
286320
}
321+
287322
if fm.fileExists(atPath: "\(productPath)/\(appName).app") {
288323
try fm.removeItem(atPath: "\(productPath)/\(appName).app")
289324
}
290325

291-
try fm.copyItem(atPath: clientAppDir, toPath: "\(productPath)/\(appName).app")
292-
326+
try fm.copyItem(atPath: clientAppURL.path, toPath: "\(productPath)/\(appName).app")
327+
293328
if package {
294329
stopwatch.record("Packaging App Bundle")
295330

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

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,30 @@ struct Codesign: AsyncParsableCommand {
1717

1818
@Flag(help: "Produce a developer build.")
1919
var developerBuild = false
20-
20+
21+
@Argument(help: "Location of the entitlements manifest for the app.")
22+
var appEntitlements: String
23+
24+
@Argument(help: "Location of the entitlements manifest for the file provider extension.")
25+
var fileProviderEntitlements: String
26+
27+
@Argument(help: "Location of the entitlements manifest for the file provider UI extension.")
28+
var fileProviderUIEntitlements: String
29+
30+
@Argument(help: "Location of the entitlements manifest for the Finder sync extension.")
31+
var finderSyncEntitlements: String
32+
2133
mutating func run() async throws {
2234
let absolutePath = appBundlePath.hasPrefix("/") ? appBundlePath : "\(FileManager.default.currentDirectoryPath)/\(appBundlePath)"
2335
let url = URL(fileURLWithPath: absolutePath)
24-
try await Signer.signMainBundle(at: url, codeSignIdentity: codeSignIdentity, developerBuild: developerBuild)
36+
37+
let entitlements = [
38+
url.lastPathComponent: URL(fileURLWithPath: appEntitlements),
39+
"FileProviderExt.appex": URL(fileURLWithPath: fileProviderEntitlements),
40+
"FileProviderUIExt.appex": URL(fileURLWithPath: fileProviderUIEntitlements),
41+
"FinderSyncExt.appex": URL(fileURLWithPath: finderSyncEntitlements),
42+
]
43+
44+
try await Signer.signMainBundle(at: url, codeSignIdentity: codeSignIdentity, entitlements: entitlements, developerBuild: developerBuild)
2545
}
2646
}

0 commit comments

Comments
 (0)