Skip to content

Consider alternative unxip implementation #63

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
## version 0.4

- Added [unxip](https://github.com/saagarjha/unxip) library for faster XIP extraction

## version 0.3

- Build scripts now produce a fat binary (multiple commits)
Expand Down
47 changes: 32 additions & 15 deletions Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,36 @@

import PackageDescription

// Define base dependencies
let baseDependencies: [Package.Dependency] = [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"),
.package(url: "https://github.com/soto-project/soto.git", from: "6.8.0"),
.package(url: "https://github.com/sebsto/CLIlib/", from: "0.1.2"),
.package(url: "https://github.com/adam-fowler/swift-srp", from: "2.1.0"),
.package(url: "https://github.com/apple/swift-crypto", from: "3.12.3")
]

// Define base products
let baseProducts: [Target.Dependency] = [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "SotoSecretsManager", package: "soto"),
.product(name: "SRP", package: "swift-srp"),
.product(name: "CLIlib", package: "CLIlib"),
.product(name: "_CryptoExtras", package: "swift-crypto")
]

#if os(macOS)
let dependencies = baseDependencies + [
.package(url: "https://github.com/saagarjha/unxip", from: "1.0.0")
]
let products = baseProducts + [
.product(name: "unxip", package: "unxip")
]
#else
let dependencies = baseDependencies
let products = baseProducts
#endif

let package = Package(
name: "xcodeinstall",
platforms: [
Expand All @@ -11,27 +41,14 @@ let package = Package(
products: [
.executable(name: "xcodeinstall", targets: ["xcodeinstall"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.5.0"),
.package(url: "https://github.com/soto-project/soto.git", from: "6.8.0"),
.package(url: "https://github.com/sebsto/CLIlib/", from: "0.1.2"),
.package(url: "https://github.com/adam-fowler/swift-srp", from: "2.1.0"),
.package(url: "https://github.com/apple/swift-crypto", from: "3.12.3"),
//.package(path: "../CLIlib")
],
dependencies: dependencies,

targets: [
// Targets are the basic building blocks of a package. A target can define a module or a test suite.
// Targets can depend on other targets in this package, and on products in packages this package depends on.
.executableTarget(
name: "xcodeinstall",
dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "SotoSecretsManager", package: "soto"),
.product(name: "SRP", package: "swift-srp"),
.product(name: "CLIlib", package: "CLIlib"),
.product(name: "_CryptoExtras", package: "swift-crypto")
]
dependencies: products
),
.testTarget(
name: "xcodeinstallTests",
Expand Down
26 changes: 26 additions & 0 deletions README.error.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
The build is failing because the unxip library depends on the Compression framework, which is only available on macOS. The error occurs when trying to build on Ubuntu Linux in the GitHub Actions workflow.

There are a few potential solutions:

1. Make the unxip integration conditional based on the platform:
- Only include unxip dependency when building on macOS
- Use fallback to the command-line xip tool on other platforms

2. Fork and modify the unxip library:
- Create a modified version that uses platform-independent compression libraries
- Update dependency to use the modified fork

3. Build only on macOS:
- Update the GitHub Actions workflow to use macOS runners
- This may increase build costs but ensures platform compatibility

The recommended solution is #1 - making the integration conditional. This allows maintaining cross-platform compatibility while still getting the performance benefits of unxip on macOS where possible.

Implementation would involve:

1. Update Package.swift to conditionally include unxip only on macOS
2. Modify InstallXcode.swift to use appropriate method based on platform
3. Update tests to handle both implementations
4. Documentation updates to clarify platform-specific behavior

This maintains the existing functionality while adding the performance improvement where supported.
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ The session stays valid for several days, sometimes weeks before it expires. Wh

> When using Secrets Manager for authentication, it is required to use it FROM THE SAME AWS REGION, for the `list` and `download` command.

## Performance Improvements

This tool uses [unxip](https://github.com/saagarjha/unxip) for extracting Xcode XIP files on macOS, which provides significantly faster extraction compared to Apple's built-in xip tool. On other platforms, it falls back to using the command line xip tool.

## Demo

![Video Demo](img/xcodeinstall-demo.gif)
Expand Down
2 changes: 1 addition & 1 deletion Sources/xcodeinstall/API/Install.swift
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ enum InstallerError: Error {
class ShellInstaller: InstallerProtocol {

// the shell commands we need to install XCode and its command line tools
let XIPCOMMAND = "/usr/bin/xip"
let XIPCOMMAND = "/usr/bin/xip" // Used as fallback when unxip is not available
let HDIUTILCOMMAND = "/usr/bin/hdiutil"
let INSTALLERCOMMAND = "/usr/sbin/installer"

Expand Down
44 changes: 35 additions & 9 deletions Sources/xcodeinstall/API/InstallXcode.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@

import CLIlib
import Foundation
#if os(macOS)
import unxip
#endif

// MARK: XCODE
// XCode installation functions
Expand Down Expand Up @@ -79,9 +82,8 @@ extension ShellInstaller {

}

// expand a XIP file. There is no way to create XIP file.
// This code can not be tested without a valid, signed, Xcode archive
// https://en.wikipedia.org/wiki/.XIP
// expand a XIP file using the faster unxip library
// https://github.com/saagarjha/unxip
func uncompressXIP(atURL file: URL) throws -> ShellOutput {

let filePath = file.path
Expand All @@ -92,13 +94,37 @@ extension ShellInstaller {
throw InstallerError.fileDoesNotExistOrIncorrect
}

// synchronously uncompress in the download directory
let cmd =
"pushd \"\(FileHandler.downloadDirectory.path)\" && "
+ "\(XIPCOMMAND) --expand \"\(filePath)\" && " + "popd"
let result = try env.shell.run(cmd)
var shellOutput: ShellOutput

#if os(macOS)
do {
// Use unxip library on macOS
let originalWorkingDirectory = FileManager.default.currentDirectoryPath
FileManager.default.changeCurrentDirectoryPath(FileHandler.downloadDirectory.path)

log.debug("Using unxip library to extract \(filePath)")
let unxipper = try Unxipper(url: file)
try unxipper.extract()

FileManager.default.changeCurrentDirectoryPath(originalWorkingDirectory)
shellOutput = ShellOutput(code: 0, stdout: "Successfully extracted \(filePath) using unxip library", stderr: "")
} catch {
log.error("Failed to extract XIP file: \(error)")
throw InstallerError.xCodeXIPInstallationError
}
#else
// Use command line xip tool on non-macOS platforms
let cmd = "pushd \"\(FileHandler.downloadDirectory.path)\" && " +
"\(XIPCOMMAND) --expand \"\(filePath)\" && " +
"popd"
shellOutput = try env.shell.run(cmd)
if shellOutput.code != 0 {
log.error("Failed to extract XIP file using xip command")
throw InstallerError.xCodeXIPInstallationError
}
#endif

return result
return shellOutput
}

func moveApp(at src: URL) throws -> String {
Expand Down
13 changes: 13 additions & 0 deletions Sources/xcodeinstall/API/Platform.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import Foundation

/// Platform-specific utilities and constants
enum Platform {
/// Check if the current platform is macOS
static var isMacOS: Bool {
#if os(macOS)
return true
#else
return false
#endif
}
}
2 changes: 1 addition & 1 deletion Sources/xcodeinstall/Version.swift
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Generated by: scripts/version
enum Version {
static let version = "0.10.1"
static let version = "0.11.0"
}
7 changes: 6 additions & 1 deletion Tests/xcodeinstallTests/API/InstallTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,15 @@ class InstallTest: XCTestCase {
XCTAssertNoThrow(try installer.uncompressXIP(atURL: srcFile))

// then
#if os(macOS)
// On macOS, we use the unxip library
// Just verify that no error is thrown
#else
// On other platforms, verify the shell command is correct
XCTAssertTrue(mockedShell().command.contains("/usr/bin/xip --expand \"\(srcFile.path)\""))
XCTAssertTrue(mockedShell().command.hasPrefix("pushd"))
XCTAssertTrue(mockedShell().command.hasSuffix("popd"))

#endif
}

func testXIPNoFile() {
Expand Down
2 changes: 1 addition & 1 deletion VERSION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
0.10.1
0.11.0
Loading