Skip to content

Commit

Permalink
Enable Xcode 16 plugin to use versioned release build (#109)
Browse files Browse the repository at this point in the history
  • Loading branch information
dfed authored Sep 3, 2024
1 parent 78e9d87 commit 96b65d5
Show file tree
Hide file tree
Showing 4 changed files with 233 additions and 15 deletions.
18 changes: 18 additions & 0 deletions Package@swift-6.0.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ let package = Package(
name: "SafeDIGenerator",
targets: ["SafeDIGenerator"]
),
.plugin(
name: "InstallSafeDITool",
targets: ["InstallSafeDITool"]
),
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"),
Expand Down Expand Up @@ -110,6 +114,20 @@ let package = Package(
.swiftLanguageMode(.v6),
]
),
.plugin(
name: "InstallSafeDITool",
capability: .command(
intent: .custom(
verb: "safedi-release-install",
description: "Installs a release version of the SafeDITool build plugin executable."
),
permissions: [
.writeToPackageDirectory(reason: "Downloads the SafeDI release build plugin executable into your project directory."),
.allowNetworkConnections(scope: .all(ports: []), reason: "Downloads the SafeDI release build plugin executable from GitHub."),
]
),
dependencies: []
),

// Core
.target(
Expand Down
110 changes: 110 additions & 0 deletions Plugins/InstallSafeDITool/InstallCLIPluginCommand.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Distributed under the MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import Foundation
import PackagePlugin

@main
struct InstallSafeDITool: CommandPlugin {
func performCommand(
context: PackagePlugin.PluginContext,
arguments _: [String]
) async throws {
guard let safeDIOrigin = context.package.dependencies.first(where: { $0.package.displayName == "SafeDI" })?.package.origin else {
Diagnostics.error("No package origin found for SafeDI package")
exit(1)
}
switch safeDIOrigin {
case let .repository(url, displayVersion, _):
// As of Xcode 16.0 Beta 6, the display version is of the form "Optional(version)".
// This regular expression is duplicated by SafeDIGenerateDependencyTree since plugins can not share code.
guard let versionMatch = try /Optional\((.*?)\)|^(.*?)$/.firstMatch(in: displayVersion),
let versionSubstring = versionMatch.output.1 ?? versionMatch.output.2
else {
Diagnostics.error("Could not extract version for SafeDI")
exit(1)
}
let version = String(versionSubstring)
let safediFolder = context.package.directoryURL.appending(
component: ".safedi"
)
let expectedToolFolder = safediFolder.appending(
component: version
)
let expectedToolLocation = expectedToolFolder.appending(component: "safeditool")

guard let url = URL(string: url)?.deletingPathExtension() else {
Diagnostics.error("No package url found for SafeDI package")
exit(1)
}
#if arch(arm64)
let toolName = "SafeDITool-arm64"
#elseif arch(x86_64)
let toolName = "SafeDITool-x86_64"
#else
Diagnostics.error("Unexpected architecture type")
exit(1)
#endif

let githubDownloadURL = url.appending(
components: "releases",
"download",
version,
toolName
)
let (downloadedURL, _) = try await URLSession.shared.download(
for: URLRequest(url: githubDownloadURL)
)
let downloadedFileAttributes = try FileManager.default.attributesOfItem(atPath: downloadedURL.path())
guard let currentPermissions = downloadedFileAttributes[.posixPermissions] as? NSNumber,
// Add executable attributes to the downloaded file.
chmod(downloadedURL.path(), mode_t(currentPermissions.uint16Value | S_IXUSR | S_IXGRP | S_IXOTH)) == 0
else {
Diagnostics.error("Failed to make downloaded file \(downloadedURL.path()) executable")
exit(1)
}
try FileManager.default.createDirectory(
at: expectedToolFolder,
withIntermediateDirectories: true
)
try FileManager.default.moveItem(
at: downloadedURL,
to: expectedToolLocation
)
let gitIgnoreLocation = safediFolder.appending(component: ".gitignore")
if !FileManager.default.fileExists(atPath: gitIgnoreLocation.path()) {
try """
*/\(expectedToolLocation.lastPathComponent)
""".write(
to: gitIgnoreLocation,
atomically: true,
encoding: .utf8
)
}

case .registry, .root, .local:
fallthrough

@unknown default:
Diagnostics.error("Cannot download SafeDITool from \(safeDIOrigin) – downloading only works when using a versioned release of SafeDI")
exit(1)
}
}
}
110 changes: 95 additions & 15 deletions Plugins/SafeDIGenerator/SafeDIGenerateDependencyTree.swift
Original file line number Diff line number Diff line change
@@ -1,3 +1,23 @@
// Distributed under the MIT License
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.

import Foundation
import PackagePlugin

Expand Down Expand Up @@ -39,16 +59,33 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin {
outputSwiftFile.path(),
]

let toolPath: URL = if FileManager.default.fileExists(atPath: Self.armMacBrewInstallLocation) {
// SafeDITool has been installed via homebrew on an ARM Mac.
URL(filePath: Self.armMacBrewInstallLocation)
} else if FileManager.default.fileExists(atPath: Self.intelMacBrewInstallLocation) {
// SafeDITool has been installed via homebrew on an Intel Mac.
URL(filePath: Self.intelMacBrewInstallLocation)
let downloadedToolLocation = context.downloadedToolLocation
let safeDIVersion = context.safeDIVersion
if context.hasSafeDIFolder, let safeDIVersion, downloadedToolLocation == nil {
Diagnostics.error("""
\(context.safediFolder.path()) exists, but contains no SafeDITool binary for version \(safeDIVersion).
To download the release SafeDITool binary for version \(safeDIVersion), run:
\tswift package --package-path \(context.package.directoryURL.path()) --allow-network-connections all --allow-writing-to-package-directory safedi-release-install
To use a debug SafeDITool binary instead, remove the `.safedi` directory by running:
\trm -rf \(context.safediFolder.path())
""")
} else if downloadedToolLocation == nil, let safeDIVersion {
Diagnostics.warning("""
Using a debug SafeDITool binary, which is 15x slower than a release SafeDITool binary.
To download the release SafeDITool binary for version \(safeDIVersion), run:
\tswift package --package-path \(context.package.directoryURL.path()) --allow-network-connections all --allow-writing-to-package-directory safedi-release-install
""")
}

let toolLocation = if let downloadedToolLocation {
downloadedToolLocation
} else {
// Fall back to the just-in-time built tool.
try context.tool(named: "SafeDITool").url
}

#else
let outputSwiftFile = context.pluginWorkDirectory.appending(subpath: "SafeDI.swift")
// Swift Package Plugins do not (as of Swift 5.9) allow for
Expand Down Expand Up @@ -79,12 +116,14 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin {
outputSwiftFile.string,
]

let toolPath: PackagePlugin.Path = if FileManager.default.fileExists(atPath: Self.armMacBrewInstallLocation) {
let armMacBrewInstallLocation = "/opt/homebrew/bin/safeditool"
let intelMacBrewInstallLocation = "/usr/local/bin/safeditool"
let toolLocation: PackagePlugin.Path = if FileManager.default.fileExists(atPath: armMacBrewInstallLocation) {
// SafeDITool has been installed via homebrew on an ARM Mac.
PackagePlugin.Path(Self.armMacBrewInstallLocation)
} else if FileManager.default.fileExists(atPath: Self.intelMacBrewInstallLocation) {
PackagePlugin.Path(armMacBrewInstallLocation)
} else if FileManager.default.fileExists(atPath: intelMacBrewInstallLocation) {
// SafeDITool has been installed via homebrew on an Intel Mac.
PackagePlugin.Path(Self.intelMacBrewInstallLocation)
PackagePlugin.Path(intelMacBrewInstallLocation)
} else {
// Fall back to the just-in-time built tool.
try context.tool(named: "SafeDITool").path
Expand All @@ -94,17 +133,14 @@ struct SafeDIGenerateDependencyTree: BuildToolPlugin {
return [
.buildCommand(
displayName: "SafeDIGenerateDependencyTree",
executable: toolPath,
executable: toolLocation,
arguments: arguments,
environment: [:],
inputFiles: targetSwiftFiles + dependenciesSourceFiles,
outputFiles: [outputSwiftFile]
),
]
}

private static let armMacBrewInstallLocation = "/opt/homebrew/bin/safeditool"
private static let intelMacBrewInstallLocation = "/usr/local/bin/safeditool"
}

extension Target {
Expand Down Expand Up @@ -257,3 +293,47 @@ extension Data {
#endif
}
}

#if compiler(>=6.0)
extension PackagePlugin.PluginContext {
var safeDIVersion: String? {
guard let safeDIOrigin = package.dependencies.first(where: { $0.package.displayName == "SafeDI" })?.package.origin else {
return nil
}
switch safeDIOrigin {
case let .repository(_, displayVersion, _):
// This regular expression is duplicated by InstallSafeDITool since plugins can not share code.
guard let versionMatch = try? /Optional\((.*?)\)|^(.*?)$/.firstMatch(in: displayVersion),
let version = versionMatch.output.1 ?? versionMatch.output.2
else {
return nil
}
return String(version)
case .registry, .root, .local:
fallthrough
@unknown default:
return nil
}
}

var hasSafeDIFolder: Bool {
FileManager.default.fileExists(atPath: safediFolder.path())
}

var safediFolder: URL {
package.directoryURL.appending(
component: ".safedi"
)
}

var downloadedToolLocation: URL? {
guard let safeDIVersion else { return nil }
let location = safediFolder.appending(
components: safeDIVersion,
"safeditool"
)
guard FileManager.default.fileExists(atPath: location.path()) else { return nil }
return location
}
}
#endif
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -437,12 +437,22 @@ If your first-party code is entirely contained in a Swift Package with one or mo

You can see this in integration in practice in the [ExamplePackageIntegration](Examples/ExamplePackageIntegration) package.

##### Xcode 15

For faster builds, you can install a release version of `SafeDITool` [rather than a debug version](https://github.com/apple/swift-package-manager/issues/7233) via `brew`:

```zsh
brew install dfed/safedi/safeditool
```

##### Xcode 16

for faster builds, you can install a release version of `SafeDITool` [rather than a debug version](https://github.com/apple/swift-package-manager/issues/7233) via `swift`:

```zsh
swift package --allow-network-connections all --allow-writing-to-package-directory safedi-release-install
```

#### Other configurations

If your first-party code comprises multiple modules in Xcode, or a mix of Xcode Projects and Swift Packages, or some other configuration not listed above, once your Xcode project depends on the SafeDI package you will need to utilize the `SafeDITool` command-line executable directly in a pre-build script.
Expand Down

0 comments on commit 96b65d5

Please sign in to comment.