diff --git a/Sources/LinuxPlatform/Linux.swift b/Sources/LinuxPlatform/Linux.swift index cdc49088..6048f59e 100644 --- a/Sources/LinuxPlatform/Linux.swift +++ b/Sources/LinuxPlatform/Linux.swift @@ -275,12 +275,11 @@ public struct Linux: Platform { try await fs.withTemporary(files: tmpFile) { try await ctx.httpClient.getGpgKeys().download(to: tmpFile) if let mockedHomeDir = ctx.mockedHomeDir { - try self.runProgram( - "gpg", "--import", "\(tmpFile)", quiet: true, - env: ["GNUPGHOME": (mockedHomeDir / ".gnupg").string] - ) + var env = ProcessInfo.processInfo.environment + env["GNUPGHOME"] = (mockedHomeDir / ".gnupg").string + try await sys.gpg()._import(keys: tmpFile).run(self, env: env, quiet: true) } else { - try self.runProgram("gpg", "--import", "\(tmpFile)", quiet: true) + try await sys.gpg()._import(keys: tmpFile).run(self, quiet: true) } } } @@ -417,12 +416,11 @@ public struct Linux: Platform { await ctx.print("Verifying toolchain signature...") do { if let mockedHomeDir = ctx.mockedHomeDir { - try self.runProgram( - "gpg", "--verify", "\(sigFile)", "\(archive)", quiet: false, - env: ["GNUPGHOME": (mockedHomeDir / ".gnupg").string] - ) + var env = ProcessInfo.processInfo.environment + env["GNUPGHOME"] = (mockedHomeDir / ".gnupg").string + try await sys.gpg().verify(detachedSignature: sigFile, signedData: archive).run(self, env: env, quiet: false) } else { - try self.runProgram("gpg", "--verify", "\(sigFile)", "\(archive)", quiet: !verbose) + try await sys.gpg().verify(detachedSignature: sigFile, signedData: archive).run(self, quiet: !verbose) } } catch { throw SwiftlyError(message: "Signature verification failed: \(error).") @@ -447,12 +445,11 @@ public struct Linux: Platform { await ctx.print("Verifying swiftly signature...") do { if let mockedHomeDir = ctx.mockedHomeDir { - try self.runProgram( - "gpg", "--verify", "\(sigFile)", "\(archive)", quiet: false, - env: ["GNUPGHOME": (mockedHomeDir / ".gnupg").string] - ) + var env = ProcessInfo.processInfo.environment + env["GNUPGHOME"] = (mockedHomeDir / ".gnupg").string + try await sys.gpg().verify(detachedSignature: sigFile, signedData: archive).run(self, env: env, quiet: false) } else { - try self.runProgram("gpg", "--verify", "\(sigFile)", "\(archive)", quiet: !verbose) + try await sys.gpg().verify(detachedSignature: sigFile, signedData: archive).run(self, quiet: !verbose) } } catch { throw SwiftlyError(message: "Signature verification failed: \(error).") diff --git a/Sources/MacOSPlatform/MacOS.swift b/Sources/MacOSPlatform/MacOS.swift index 6d289b81..dd72af1c 100644 --- a/Sources/MacOSPlatform/MacOS.swift +++ b/Sources/MacOSPlatform/MacOS.swift @@ -70,10 +70,8 @@ public struct MacOS: Platform { if toolchainsDir == self.defaultToolchainsDirectory { // If the toolchains go into the default user location then we use the installer to install them await ctx.print("Installing package in user home directory...") - try runProgram( - "installer", "-verbose", "-pkg", "\(tmpFile)", "-target", "CurrentUserHomeDirectory", - quiet: !verbose - ) + + try await sys.installer(.verbose, pkg: tmpFile, target: "CurrentUserHomeDirectory").run(self, quiet: !verbose) } else { // Otherwise, we extract the pkg into the requested toolchains directory. await ctx.print("Expanding pkg...") @@ -86,7 +84,7 @@ public struct MacOS: Platform { await ctx.print("Checking package signature...") do { - try runProgram("pkgutil", "--check-signature", "\(tmpFile)", quiet: !verbose) + try await sys.pkgutil().checkSignature(pkgPath: tmpFile).run(self, quiet: !verbose) } catch { // If this is not a test that uses mocked toolchains then we must throw this error and abort installation guard ctx.mockedHomeDir != nil else { @@ -96,7 +94,7 @@ public struct MacOS: Platform { // We permit the signature verification to fail during testing await ctx.print("Signature verification failed, which is allowable during testing with mocked toolchains") } - try runProgram("pkgutil", "--verbose", "--expand", "\(tmpFile)", "\(tmpDir)", quiet: !verbose) + try await sys.pkgutil(.verbose).expand(pkgPath: tmpFile, dirPath: tmpDir).run(self, quiet: !verbose) // There's a slight difference in the location of the special Payload file between official swift packages // and the ones that are mocked here in the test framework. @@ -106,7 +104,7 @@ public struct MacOS: Platform { } await ctx.print("Untarring pkg Payload...") - try runProgram("tar", "-C", "\(toolchainDir)", "-xvf", "\(payload)", quiet: !verbose) + try await sys.tar(.directory(toolchainDir)).extract(.verbose, .archive(payload)).run(self, quiet: !verbose) } } @@ -119,8 +117,11 @@ public struct MacOS: Platform { if ctx.mockedHomeDir == nil { await ctx.print("Extracting the swiftly package...") - try runProgram("installer", "-pkg", "\(archive)", "-target", "CurrentUserHomeDirectory") - try? runProgram("pkgutil", "--volume", "\(userHomeDir)", "--forget", "org.swift.swiftly") + try await sys.installer( + pkg: archive, + target: "CurrentUserHomeDirectory" + ) + try? await sys.pkgutil(.volume(userHomeDir)).forget(packageId: "org.swift.swiftly").run(self) } else { let installDir = userHomeDir / ".swiftly" try await fs.mkdir(.parents, atPath: installDir) @@ -128,7 +129,7 @@ public struct MacOS: Platform { // In the case of a mock for testing purposes we won't use the installer, perferring a manual process because // the installer will not install to an arbitrary path, only a volume or user home directory. let tmpDir = fs.mktemp() - try runProgram("pkgutil", "--expand", "\(archive)", "\(tmpDir)") + try await sys.pkgutil().expand(pkgPath: archive, dirPath: tmpDir).run(self) // There's a slight difference in the location of the special Payload file between official swift packages // and the ones that are mocked here in the test framework. @@ -138,7 +139,7 @@ public struct MacOS: Platform { } await ctx.print("Extracting the swiftly package into \(installDir)...") - try runProgram("tar", "-C", "\(installDir)", "-xvf", "\(payload)", quiet: false) + try await sys.tar(.directory(installDir)).extract(.verbose, .archive(payload)).run(self, quiet: false) } try self.runProgram((userHomeDir / ".swiftly/bin/swiftly").string, "init") @@ -161,9 +162,7 @@ public struct MacOS: Platform { try await fs.remove(atPath: toolchainDir) - try? runProgram( - "pkgutil", "--volume", "\(fs.home)", "--forget", pkgInfo.CFBundleIdentifier, quiet: !verbose - ) + try? await sys.pkgutil(.volume(fs.home)).forget(packageId: pkgInfo.CFBundleIdentifier).run(self, quiet: !verbose) } public func getExecutableName() -> String { diff --git a/Sources/Swiftly/Install.swift b/Sources/Swiftly/Install.swift index 50a152f4..0310adad 100644 --- a/Sources/Swiftly/Install.swift +++ b/Sources/Swiftly/Install.swift @@ -261,7 +261,7 @@ struct Install: SwiftlyCommand { await ctx.print("Installing \(version)") - let tmpFile = fs.mktemp() + let tmpFile = fs.mktemp(ext: ".\(Swiftly.currentPlatform.toolchainFileExtension)") try await fs.create(file: tmpFile, contents: nil) return try await fs.withTemporary(files: tmpFile) { var platformString = config.platform.name diff --git a/Sources/SwiftlyCore/Commands.swift b/Sources/SwiftlyCore/Commands.swift index 83826ff1..bb4a81b5 100644 --- a/Sources/SwiftlyCore/Commands.swift +++ b/Sources/SwiftlyCore/Commands.swift @@ -5,9 +5,9 @@ public enum SystemCommand {} // This file contains a set of system commands that's used by Swiftly and its related tests and tooling -// Directory Service command line utility for macOS -// See dscl(1) for details extension SystemCommand { + // Directory Service command line utility for macOS + // See dscl(1) for details public static func dscl(executable: Executable = DsclCommand.defaultExecutable, datasource: String? = nil) -> DsclCommand { DsclCommand(executable: executable, datasource: datasource) } @@ -93,13 +93,15 @@ extension SystemCommand.DsclCommand.ReadCommand: Output { } } -// Create or operate on universal files -// See lipo(1) for details extension SystemCommand { + // Create or operate on universal files + // See lipo(1) for details public static func lipo(executable: Executable = LipoCommand.defaultExecutable, inputFiles: FilePath...) -> LipoCommand { Self.lipo(executable: executable, inputFiles: inputFiles) } + // Create or operate on universal files + // See lipo(1) for details public static func lipo(executable: Executable = LipoCommand.defaultExecutable, inputFiles: [FilePath]) -> LipoCommand { LipoCommand(executable: executable, inputFiles: inputFiles) } @@ -155,13 +157,15 @@ extension SystemCommand { extension SystemCommand.LipoCommand.CreateCommand: Runnable {} -// Build a macOS Installer component package from on-disk files -// See pkgbuild(1) for more details extension SystemCommand { + // Build a macOS Installer component package from on-disk files + // See pkgbuild(1) for more details public static func pkgbuild(executable: Executable = PkgbuildCommand.defaultExecutable, _ options: PkgbuildCommand.Option..., root: FilePath, packageOutputPath: FilePath) -> PkgbuildCommand { Self.pkgbuild(executable: executable, options: options, root: root, packageOutputPath: packageOutputPath) } + // Build a macOS Installer component package from on-disk files + // See pkgbuild(1) for more details public static func pkgbuild(executable: Executable = PkgbuildCommand.defaultExecutable, options: [PkgbuildCommand.Option], root: FilePath, packageOutputPath: FilePath) -> PkgbuildCommand { PkgbuildCommand(executable: executable, options, root: root, packageOutputPath: packageOutputPath) } @@ -224,13 +228,15 @@ extension SystemCommand { extension SystemCommand.PkgbuildCommand: Runnable {} -// get entries from Name Service Switch libraries -// See getent(1) for more details extension SystemCommand { + // get entries from Name Service Switch libraries + // See getent(1) for more details public static func getent(executable: Executable = GetentCommand.defaultExecutable, database: String, keys: String...) -> GetentCommand { Self.getent(executable: executable, database: database, keys: keys) } + // get entries from Name Service Switch libraries + // See getent(1) for more details public static func getent(executable: Executable = GetentCommand.defaultExecutable, database: String, keys: [String]) -> GetentCommand { GetentCommand(executable: executable, database: database, keys: keys) } @@ -283,6 +289,8 @@ extension SystemCommand.GetentCommand: Output { } extension SystemCommand { + // the stupid content tracker + // See git(1) for more information. public static func git(executable: Executable = GitCommand.defaultExecutable, workingDir: FilePath? = nil) -> GitCommand { GitCommand(executable: executable, workingDir: workingDir) } @@ -485,3 +493,1037 @@ extension SystemCommand.GitCommand.LogCommand: Output {} extension SystemCommand.GitCommand.DiffIndexCommand: Runnable {} extension SystemCommand.GitCommand.InitCommand: Runnable {} extension SystemCommand.GitCommand.CommitCommand: Runnable {} + +extension SystemCommand { + // manipulate tape archives + // See tar(1) for more details + public static func tar(executable: Executable = TarCommand.defaultExecutable, _ options: TarCommand.Option...) -> TarCommand { + Self.tar(executable: executable, options) + } + + // manipulate tape archives + // See tar(1) for more details + public static func tar(executable: Executable = TarCommand.defaultExecutable, _ options: [TarCommand.Option]) -> TarCommand { + TarCommand(executable: executable, options) + } + + public struct TarCommand { + public static var defaultExecutable: Executable { .name("tar") } + + var executable: Executable + + var options: [Option] + + public init(executable: Executable, _ options: [Option]) { + self.executable = executable + self.options = options + } + + public enum Option { + case directory(FilePath) + + public func args() -> [String] { + switch self { + case let .directory(directory): + return ["-C", "\(directory)"] // This is the only portable form between macOS and GNU + } + } + } + + func config() -> Configuration { + var args: [String] = [] + + for opt in self.options { + args.append(contentsOf: opt.args()) + } + + return Configuration( + executable: self.executable, + arguments: Arguments(args), + environment: .inherit + ) + } + + public func create(_ options: CreateCommand.Option...) -> CreateCommand { + self.create(options, files: []) + } + + public func create(_ options: CreateCommand.Option..., files: FilePath...) -> CreateCommand { + self.create(options, files: files) + } + + public func create(_ options: [CreateCommand.Option], files: [FilePath]) -> CreateCommand { + CreateCommand(self, options, files: files) + } + + public struct CreateCommand { + var tar: TarCommand + + var options: [Option] + + var files: [FilePath] + + init(_ tar: TarCommand, _ options: [Option], files: [FilePath]) { + self.tar = tar + self.options = options + self.files = files + } + + public enum Option { + case archive(FilePath) + case compressed + case verbose + + func args() -> [String] { + switch self { + case let .archive(archive): + return ["--file", "\(archive)"] + case .compressed: + return ["-z"] + case .verbose: + return ["-v"] + } + } + } + + public func config() -> Configuration { + var c = self.tar.config() + + var args = c.arguments.storage.map(\.description) + + args.append("-c") + + for opt in self.options { + args.append(contentsOf: opt.args()) + } + + args.append(contentsOf: self.files.map(\.string)) + + c.arguments = .init(args) + + return c + } + } + + public func extract(_ options: ExtractCommand.Option...) -> ExtractCommand { + ExtractCommand(self, options) + } + + public struct ExtractCommand { + var tar: TarCommand + + var options: [Option] + + init(_ tar: TarCommand, _ options: [Option]) { + self.tar = tar + self.options = options + } + + public enum Option { + case archive(FilePath) + case compressed + case verbose + + func args() -> [String] { + switch self { + case let .archive(archive): + return ["--file", "\(archive)"] + case .compressed: + return ["-z"] + case .verbose: + return ["-v"] + } + } + } + + public func config() -> Configuration { + var c = self.tar.config() + + var args = c.arguments.storage.map(\.description) + + args.append("-x") + + for opt in self.options { + args.append(contentsOf: opt.args()) + } + + c.arguments = .init(args) + + return c + } + } + } +} + +extension SystemCommand.TarCommand.CreateCommand: Runnable {} +extension SystemCommand.TarCommand.ExtractCommand: Runnable {} + +extension SystemCommand { + public static func swift(executable: Executable = SwiftCommand.defaultExecutable) -> SwiftCommand { + SwiftCommand(executable: executable) + } + + public struct SwiftCommand { + public static var defaultExecutable: Executable { .name("swift") } + + public var executable: Executable + + public init(executable: Executable) { + self.executable = executable + } + + func config() -> Configuration { + var args: [String] = [] + + return Configuration( + executable: self.executable, + arguments: Arguments(args), + environment: .inherit + ) + } + + public func package() -> PackageCommand { + PackageCommand(self) + } + + public struct PackageCommand { + var swift: SwiftCommand + + init(_ swift: SwiftCommand) { + self.swift = swift + } + + public func config() -> Configuration { + var c = self.swift.config() + + var args = c.arguments.storage.map(\.description) + + args.append("package") + + c.arguments = .init(args) + + return c + } + + public func reset() -> ResetCommand { + ResetCommand(self) + } + + public struct ResetCommand { + var packageCommand: PackageCommand + + init(_ packageCommand: PackageCommand) { + self.packageCommand = packageCommand + } + + public func config() -> Configuration { + var c = self.packageCommand.config() + + var args = c.arguments.storage.map(\.description) + + args.append("reset") + + c.arguments = .init(args) + + return c + } + } + + public func clean() -> CleanCommand { + CleanCommand(self) + } + + public struct CleanCommand { + var packageCommand: PackageCommand + + init(_ packageCommand: PackageCommand) { + self.packageCommand = packageCommand + } + + public func config() -> Configuration { + var c = self.packageCommand.config() + + var args = c.arguments.storage.map(\.description) + + args.append("clean") + + c.arguments = .init(args) + + return c + } + } + + public func _init(_ options: InitCommand.Option...) -> InitCommand { + self._init(options: options) + } + + public func _init(options: [InitCommand.Option]) -> InitCommand { + InitCommand(self, options) + } + + public struct InitCommand { + var packageCommand: PackageCommand + + var options: [Option] + + public enum Option { + case type(String) + case packagePath(FilePath) + + func args() -> [String] { + switch self { + case let .type(type): + return ["--type=\(type)"] + case let .packagePath(packagePath): + return ["--package-path=\(packagePath)"] + } + } + } + + init(_ packageCommand: PackageCommand, _ options: [Option]) { + self.packageCommand = packageCommand + self.options = options + } + + public func config() -> Configuration { + var c = self.packageCommand.config() + + var args = c.arguments.storage.map(\.description) + + args.append("init") + + for opt in self.options { + args.append(contentsOf: opt.args()) + } + + c.arguments = .init(args) + + return c + } + } + } + + public func sdk() -> SdkCommand { + SdkCommand(self) + } + + public struct SdkCommand { + var swift: SwiftCommand + + init(_ swift: SwiftCommand) { + self.swift = swift + } + + public func config() -> Configuration { + var c = self.swift.config() + + var args = c.arguments.storage.map(\.description) + + args.append("sdk") + + c.arguments = .init(args) + + return c + } + + public func install(_ bundlePathOrUrl: String, checksum: String? = nil) -> InstallCommand { + InstallCommand(self, bundlePathOrUrl, checksum: checksum) + } + + public struct InstallCommand { + var sdkCommand: SdkCommand + var bundlePathOrUrl: String + var checksum: String? + + init(_ sdkCommand: SdkCommand, _ bundlePathOrUrl: String, checksum: String?) { + self.sdkCommand = sdkCommand + self.bundlePathOrUrl = bundlePathOrUrl + self.checksum = checksum + } + + public func config() -> Configuration { + var c = self.sdkCommand.config() + + var args = c.arguments.storage.map(\.description) + + args.append("install") + + args.append(self.bundlePathOrUrl) + + if let checksum = self.checksum { + args.append("--checksum=\(checksum)") + } + + c.arguments = .init(args) + + return c + } + } + + public func remove(_ sdkIdOrBundleName: String) -> RemoveCommand { + RemoveCommand(self, sdkIdOrBundleName) + } + + public struct RemoveCommand { + var sdkCommand: SdkCommand + var sdkIdOrBundleName: String + + init(_ sdkCommand: SdkCommand, _ sdkIdOrBundleName: String) { + self.sdkCommand = sdkCommand + self.sdkIdOrBundleName = sdkIdOrBundleName + } + + public func config() -> Configuration { + var c = self.sdkCommand.config() + + var args = c.arguments.storage.map(\.description) + + args.append("remove") + + args.append(self.sdkIdOrBundleName) + + c.arguments = .init(args) + + return c + } + } + } + + public func build(_ options: BuildCommand.Option...) -> BuildCommand { + BuildCommand(self, options) + } + + public struct BuildCommand { + var swift: SwiftCommand + var options: [Option] + + init(_ swift: SwiftCommand, _ options: [Option]) { + self.swift = swift + self.options = options + } + + public func config() -> Configuration { + var c = self.swift.config() + + var args = c.arguments.storage.map(\.description) + + args.append("build") + + for opt in self.options { + args.append(contentsOf: opt.args()) + } + + c.arguments = .init(args) + + return c + } + + public enum Option { + case arch(String) + case configuration(String) + case packagePath(FilePath) + case pkgConfigPath(String) + case product(String) + case swiftSdk(String) + case staticSwiftStdlib + + func args() -> [String] { + switch self { + case let .arch(arch): + return ["--arch=\(arch)"] + case let .configuration(configuration): + return ["--configuration=\(configuration)"] + case let .packagePath(packagePath): + return ["--package-path=\(packagePath)"] + case let .pkgConfigPath(pkgConfigPath): + return ["--pkg-config-path=\(pkgConfigPath)"] + case let .swiftSdk(sdk): + return ["--swift-sdk=\(sdk)"] + case .staticSwiftStdlib: + return ["--static-swift-stdlib"] + case let .product(product): + return ["--product=\(product)"] + } + } + } + } + } +} + +extension SystemCommand.SwiftCommand.PackageCommand.ResetCommand: Runnable {} +extension SystemCommand.SwiftCommand.PackageCommand.CleanCommand: Runnable {} +extension SystemCommand.SwiftCommand.PackageCommand.InitCommand: Runnable {} +extension SystemCommand.SwiftCommand.SdkCommand.InstallCommand: Runnable {} +extension SystemCommand.SwiftCommand.SdkCommand.RemoveCommand: Runnable {} +extension SystemCommand.SwiftCommand.BuildCommand: Runnable {} + +extension SystemCommand { + // make utility to maintain groups of programs + // See make(1) for more information. + public static func make(executable: Executable = MakeCommand.defaultExecutable) -> MakeCommand { + MakeCommand(executable: executable) + } + + public struct MakeCommand { + public static var defaultExecutable: Executable { .name("make") } + + public var executable: Executable + + public init(executable: Executable) { + self.executable = executable + } + + public func config() -> Configuration { + var args: [String] = [] + + return Configuration( + executable: self.executable, + arguments: Arguments(args), + environment: .inherit + ) + } + + public func install() -> InstallCommand { + InstallCommand(self) + } + + public struct InstallCommand { + var make: MakeCommand + + init(_ make: MakeCommand) { + self.make = make + } + + public func config() -> Configuration { + var c = self.make.config() + + var args = c.arguments.storage.map(\.description) + + args.append("install") + + c.arguments = .init(args) + + return c + } + } + } +} + +extension SystemCommand.MakeCommand: Runnable {} +extension SystemCommand.MakeCommand.InstallCommand: Runnable {} + +extension SystemCommand { + // remove symbols + // See strip(1) for more information. + public static func strip(executable: Executable = StripCommand.defaultExecutable, names: FilePath...) -> StripCommand { + self.strip(executable: executable, names: names) + } + + // remove symbols + // See strip(1) for more information. + public static func strip(executable: Executable = StripCommand.defaultExecutable, names: [FilePath]) -> StripCommand { + StripCommand(executable: executable, names: names) + } + + public struct StripCommand { + public static var defaultExecutable: Executable { .name("strip") } + + public var executable: Executable + + public var names: [FilePath] + + public init(executable: Executable, names: [FilePath]) { + self.executable = executable + self.names = names + } + + public func config() -> Configuration { + var args: [String] = [] + + args.append(contentsOf: self.names.map(\.string)) + + return Configuration( + executable: self.executable, + arguments: Arguments(args), + environment: .inherit + ) + } + } +} + +extension SystemCommand.StripCommand: Runnable {} + +extension SystemCommand { + // calculate a message-digest fingerprint (checksum) for a file + // See sha256sum(1) for more information. + public static func sha256sum(executable: Executable = Sha256SumCommand.defaultExecutable, files: FilePath...) -> Sha256SumCommand { + self.sha256sum(executable: executable, files: files) + } + + // calculate a message-digest fingerprint (checksum) for a file + // See sha256sum(1) for more information. + public static func sha256sum(executable: Executable, files: [FilePath]) -> Sha256SumCommand { + Sha256SumCommand(executable: executable, files: files) + } + + public struct Sha256SumCommand { + public static var defaultExecutable: Executable { .name("sha256sum") } + + public var executable: Executable + + public var files: [FilePath] + + public init(executable: Executable, files: [FilePath]) { + self.executable = executable + self.files = files + } + + public func config() -> Configuration { + var args: [String] = [] + + args.append(contentsOf: self.files.map(\.string)) + + return Configuration( + executable: self.executable, + arguments: Arguments(args), + environment: .inherit + ) + } + } +} + +extension SystemCommand.Sha256SumCommand: Output {} + +extension SystemCommand { + // Build a product archive for the macOS Installer or the Mac App Store. + // See productbuild(1) for more information. + public static func productbuild(executable: Executable = ProductBuildCommand.defaultExecutable) -> ProductBuildCommand { + ProductBuildCommand(executable: executable) + } + + public struct ProductBuildCommand { + public static var defaultExecutable: Executable { .name("productbuild") } + + public var executable: Executable + + public init(executable: Executable) { + self.executable = executable + } + + public func config() -> Configuration { + var args: [String] = [] + + return Configuration( + executable: self.executable, + arguments: Arguments(args), + environment: .inherit + ) + } + + public func synthesize(package: FilePath, distributionOutputPath: FilePath) -> SynthesizeCommand { + SynthesizeCommand(self, package: package, distributionOutputPath: distributionOutputPath) + } + + public struct SynthesizeCommand { + public var productBuildCommand: ProductBuildCommand + + public var package: FilePath + + public var distributionOutputPath: FilePath + + public init(_ productBuildCommand: ProductBuildCommand, package: FilePath, distributionOutputPath: FilePath) { + self.productBuildCommand = productBuildCommand + self.package = package + self.distributionOutputPath = distributionOutputPath + } + + public func config() -> Configuration { + var c = self.productBuildCommand.config() + + var args = c.arguments.storage.map(\.description) + + args.append("--synthesize") + + args.append(contentsOf: ["--package", "\(self.package)"]) + args.append("\(self.distributionOutputPath)") + + c.arguments = .init(args) + + return c + } + } + + public func distribution(_ options: DistributionCommand.Option..., distPath: FilePath, productOutputPath: FilePath) -> DistributionCommand { + self.distribution(options, distPath: distPath, productOutputPath: productOutputPath) + } + + public func distribution(_ options: [DistributionCommand.Option], distPath: FilePath, productOutputPath: FilePath) -> DistributionCommand { + DistributionCommand(self, options, distPath: distPath, productOutputPath: productOutputPath) + } + + public struct DistributionCommand { + public var productBuildCommand: ProductBuildCommand + + public var options: [Option] + + public var distPath: FilePath + + public var productOutputPath: FilePath + + public enum Option { + case packagePath(FilePath) + case sign(String) + + public func args() -> [String] { + switch self { + case let .packagePath(packagePath): + ["--package-path", "\(packagePath)"] + case let .sign(sign): + ["--sign", "\(sign)"] + } + } + } + + public init(_ productBuildCommand: ProductBuildCommand, _ options: [Option], distPath: FilePath, productOutputPath: FilePath) { + self.productBuildCommand = productBuildCommand + self.options = options + self.distPath = distPath + self.productOutputPath = productOutputPath + } + + public func config() -> Configuration { + var c = self.productBuildCommand.config() + + var args = c.arguments.storage.map(\.description) + + args.append("--distribution") + + args.append("\(self.distPath)") + + for opt in self.options { + args.append(contentsOf: opt.args()) + } + + args.append("\(self.productOutputPath)") + + c.arguments = .init(args) + + return c + } + } + } +} + +extension SystemCommand.ProductBuildCommand.SynthesizeCommand: Runnable {} +extension SystemCommand.ProductBuildCommand.DistributionCommand: Runnable {} + +extension SystemCommand { + // OpenPGP encryption and signing tool + // See gpg(1) for more information. + public static func gpg(executable: Executable = GpgCommand.defaultExecutable) -> GpgCommand { + GpgCommand(executable: executable) + } + + public struct GpgCommand { + public static var defaultExecutable: Executable { .name("gpg") } + + public var executable: Executable + + public init(executable: Executable) { + self.executable = executable + } + + public func config() -> Configuration { + var args: [String] = [] + + return Configuration( + executable: self.executable, + arguments: Arguments(args), + environment: .inherit + ) + } + + public func _import(keys: FilePath...) -> ImportCommand { + self._import(keys: keys) + } + + public func _import(keys: [FilePath]) -> ImportCommand { + ImportCommand(self, keys: keys) + } + + public struct ImportCommand { + public var gpg: GpgCommand + + public var keys: [FilePath] + + public init(_ gpg: GpgCommand, keys: [FilePath]) { + self.gpg = gpg + self.keys = keys + } + + public func config() -> Configuration { + var c: Configuration = self.gpg.config() + + var args = c.arguments.storage.map(\.description) + + args.append("--import") + + for key in self.keys { + args.append("\(key)") + } + + c.arguments = .init(args) + + return c + } + } + + public func verify(detachedSignature: FilePath, signedData: FilePath) -> VerifyCommand { + VerifyCommand(self, detachedSignature: detachedSignature, signedData: signedData) + } + + public struct VerifyCommand { + public var gpg: GpgCommand + + public var detachedSignature: FilePath + + public var signedData: FilePath + + public init(_ gpg: GpgCommand, detachedSignature: FilePath, signedData: FilePath) { + self.gpg = gpg + self.detachedSignature = detachedSignature + self.signedData = signedData + } + + public func config() -> Configuration { + var c: Configuration = self.gpg.config() + + var args = c.arguments.storage.map(\.description) + + args.append("--verify") + + args.append("\(self.detachedSignature)") + args.append("\(self.signedData)") + + c.arguments = .init(args) + + return c + } + } + } +} + +extension SystemCommand.GpgCommand.ImportCommand: Runnable {} +extension SystemCommand.GpgCommand.VerifyCommand: Runnable {} + +extension SystemCommand { + // Query and manipulate macOS Installer packages and receipts. + // See pkgutil(1) for more information. + public static func pkgutil(executable: Executable = PkgutilCommand.defaultExecutable, _ options: PkgutilCommand.Option...) -> PkgutilCommand { + Self.pkgutil(executable: executable, options) + } + + // Query and manipulate macOS Installer packages and receipts. + // See pkgutil(1) for more information. + public static func pkgutil(executable: Executable = PkgutilCommand.defaultExecutable, _ options: [PkgutilCommand.Option]) -> PkgutilCommand { + PkgutilCommand(executable: executable, options) + } + + public struct PkgutilCommand { + public static var defaultExecutable: Executable { .name("pkgutil") } + + public var executable: Executable + + public var options: [Option] + + public enum Option { + case verbose + case volume(FilePath) + + public func args() -> [String] { + switch self { + case .verbose: + ["--verbose"] + case let .volume(volume): + ["--volume", "\(volume)"] + } + } + } + + public init(executable: Executable, _ options: [Option]) { + self.executable = executable + self.options = options + } + + public func config() -> Configuration { + var args: [String] = [] + + for opt in self.options { + args.append(contentsOf: opt.args()) + } + + return Configuration( + executable: self.executable, + arguments: Arguments(args), + environment: .inherit + ) + } + + public func checkSignature(pkgPath: FilePath) -> CheckSignatureCommand { + CheckSignatureCommand(self, pkgPath: pkgPath) + } + + public struct CheckSignatureCommand { + public var pkgutil: PkgutilCommand + + public var pkgPath: FilePath + + public init(_ pkgutil: PkgutilCommand, pkgPath: FilePath) { + self.pkgutil = pkgutil + self.pkgPath = pkgPath + } + + public func config() -> Configuration { + var c: Configuration = self.pkgutil.config() + + var args = c.arguments.storage.map(\.description) + + args.append("--check-signature") + + args.append("\(self.pkgPath)") + + c.arguments = .init(args) + + return c + } + } + + public func expand(pkgPath: FilePath, dirPath: FilePath) -> ExpandCommand { + ExpandCommand(self, pkgPath: pkgPath, dirPath: dirPath) + } + + public struct ExpandCommand { + public var pkgutil: PkgutilCommand + + public var pkgPath: FilePath + + public var dirPath: FilePath + + public init(_ pkgutil: PkgutilCommand, pkgPath: FilePath, dirPath: FilePath) { + self.pkgutil = pkgutil + self.pkgPath = pkgPath + self.dirPath = dirPath + } + + public func config() -> Configuration { + var c: Configuration = self.pkgutil.config() + + var args = c.arguments.storage.map(\.description) + + args.append("--expand") + + args.append("\(self.pkgPath)") + + args.append("\(self.dirPath)") + + c.arguments = .init(args) + + return c + } + } + + public func forget(packageId: String) -> ForgetCommand { + ForgetCommand(self, packageId: packageId) + } + + public struct ForgetCommand { + public var pkgutil: PkgutilCommand + + public var packageId: String + + public init(_ pkgutil: PkgutilCommand, packageId: String) { + self.pkgutil = pkgutil + self.packageId = packageId + } + + public func config() -> Configuration { + var c: Configuration = self.pkgutil.config() + + var args = c.arguments.storage.map(\.description) + + args.append("--forget") + + args.append("\(self.packageId)") + + c.arguments = .init(args) + + return c + } + } + } +} + +extension SystemCommand.PkgutilCommand.CheckSignatureCommand: Runnable {} +extension SystemCommand.PkgutilCommand.ExpandCommand: Runnable {} +extension SystemCommand.PkgutilCommand.ForgetCommand: Runnable {} + +extension SystemCommand { + // system software and package installer tool. + // See installer(1) for more information + public static func installer(executable: Executable = InstallerCommand.defaultExecutable, _ options: InstallerCommand.Option..., pkg: FilePath, target: String) -> InstallerCommand { + self.installer(executable: executable, options, pkg: pkg, target: target) + } + + public static func installer(executable: Executable = InstallerCommand.defaultExecutable, _ options: [InstallerCommand.Option], pkg: FilePath, target: String) -> InstallerCommand { + InstallerCommand(executable: executable, options, pkg: pkg, target: target) + } + + public struct InstallerCommand { + public static var defaultExecutable: Executable { .name("installer") } + + public var executable: Executable + + public var options: [Option] + + public var pkg: FilePath + + public var target: String + + public enum Option { + case verbose + + public func args() -> [String] { + switch self { + case .verbose: + ["-verbose"] + } + } + } + + public init(executable: Executable, _ options: [Option], pkg: FilePath, target: String) { + self.executable = executable + self.options = options + self.pkg = pkg + self.target = target + } + + public func config() -> Configuration { + var args: [String] = [] + + for opt in self.options { + args.append(contentsOf: opt.args()) + } + + args.append(contentsOf: ["-pkg", "\(self.pkg)"]) + args.append(contentsOf: ["-target", self.target]) + + return Configuration( + executable: self.executable, + arguments: Arguments(args), + environment: .inherit + ) + } + } +} + +extension SystemCommand.InstallerCommand: Runnable {} diff --git a/Sources/SwiftlyCore/ModeledCommandLine.swift b/Sources/SwiftlyCore/ModeledCommandLine.swift index 9432b81b..42bc4c5b 100644 --- a/Sources/SwiftlyCore/ModeledCommandLine.swift +++ b/Sources/SwiftlyCore/ModeledCommandLine.swift @@ -154,7 +154,7 @@ public protocol Runnable { } extension Runnable { - public func run(_ p: Platform, quiet: Bool = false) async throws { + public func run(_ p: Platform, env: [String: String] = ProcessInfo.processInfo.environment, quiet: Bool = false) async throws { let c = self.config() let executable = switch c.executable.storage { case let .executable(name): @@ -163,16 +163,19 @@ extension Runnable { p.string } let args = c.arguments.storage.map(\.description) - var env: [String: String] = ProcessInfo.processInfo.environment + + var newEnv: [String: String] = env + switch c.environment.config { case let .inherit(newValue): for (key, value) in newValue { - env[key] = value + newEnv[key] = value } case let .custom(newValue): - env = newValue + newEnv = newValue } - try await p.runProgram([executable] + args, quiet: quiet, env: env) + + try await p.runProgram([executable] + args, quiet: quiet, env: newEnv) } } diff --git a/Tests/SwiftlyTests/CommandLineTests.swift b/Tests/SwiftlyTests/CommandLineTests.swift index acf65c4c..98c6c93d 100644 --- a/Tests/SwiftlyTests/CommandLineTests.swift +++ b/Tests/SwiftlyTests/CommandLineTests.swift @@ -1,3 +1,4 @@ +import Foundation @testable import Swiftly @testable import SwiftlyCore import SystemPackage @@ -111,4 +112,136 @@ public struct CommandLineTests { try await sys.git(workingDir: tmp).diffIndex(.quiet, treeIsh: "HEAD").run(Swiftly.currentPlatform) } } + + @Test func testTarModel() { + var config = sys.tar(.directory("/some/cool/stuff")).create(.compressed, .archive("abc.tgz"), files: "a", "b").config() + #expect(String(describing: config) == "tar -C /some/cool/stuff -c -z --file abc.tgz a b") + + config = sys.tar().create(.archive("myarchive.tar")).config() + #expect(String(describing: config) == "tar -c --file myarchive.tar") + + config = sys.tar(.directory("/this/is/the/place")).extract(.compressed, .archive("def.tgz")).config() + #expect(String(describing: config) == "tar -C /this/is/the/place -x -z --file def.tgz") + + config = sys.tar().extract(.archive("somearchive.tar")).config() + #expect(String(describing: config) == "tar -x --file somearchive.tar") + } + + @Test( + .tags(.medium), + .enabled { + try await sys.TarCommand.defaultExecutable.exists() + } + ) + func testTar() async throws { + let tmp = fs.mktemp() + try await fs.mkdir(atPath: tmp) + let readme = "README.md" + try await "README".write(to: tmp / readme, atomically: true) + + let arch = fs.mktemp(ext: "tar") + let archCompressed = fs.mktemp(ext: "tgz") + + try await sys.tar(.directory(tmp)).create(.verbose, .archive(arch), files: FilePath(readme)).run(Swiftly.currentPlatform) + try await sys.tar(.directory(tmp)).create(.verbose, .compressed, .archive(archCompressed), files: FilePath(readme)).run(Swiftly.currentPlatform) + + let tmp2 = fs.mktemp() + try await fs.mkdir(atPath: tmp2) + + try await sys.tar(.directory(tmp2)).extract(.verbose, .archive(arch)).run(Swiftly.currentPlatform) + + let contents = try await String(contentsOf: tmp2 / readme, encoding: .utf8) + #expect(contents == "README") + + let tmp3 = fs.mktemp() + try await fs.mkdir(atPath: tmp3) + + try await sys.tar(.directory(tmp3)).extract(.verbose, .compressed, .archive(archCompressed)).run(Swiftly.currentPlatform) + + let contents2 = try await String(contentsOf: tmp3 / readme, encoding: .utf8) + #expect(contents2 == "README") + } + + @Test func testSwiftModel() async throws { + var config = sys.swift().package().reset().config() + #expect(String(describing: config) == "swift package reset") + + config = sys.swift().package().clean().config() + #expect(String(describing: config) == "swift package clean") + + config = sys.swift().sdk().install("path/to/bundle", checksum: "deadbeef").config() + #expect(String(describing: config) == "swift sdk install path/to/bundle --checksum=deadbeef") + + config = sys.swift().sdk().remove("some.bundle").config() + #expect(String(describing: config) == "swift sdk remove some.bundle") + + config = sys.swift().build(.arch("x86_64"), .configuration("release"), .pkgConfigPath("path/to/pc"), .swiftSdk("sdk.id"), .staticSwiftStdlib, .product("product1")).config() + #expect(String(describing: config) == "swift build --arch=x86_64 --configuration=release --pkg-config-path=path/to/pc --swift-sdk=sdk.id --static-swift-stdlib --product=product1") + + config = sys.swift().build().config() + #expect(String(describing: config) == "swift build") + } + + @Test( + .tags(.medium), + .enabled { + try await sys.SwiftCommand.defaultExecutable.exists() + } + ) + func testSwift() async throws { + let tmp = fs.mktemp() + try await fs.mkdir(atPath: tmp) + try await sys.swift().package()._init(.packagePath(tmp), .type("executable")).run(Swiftly.currentPlatform) + try await sys.swift().build(.packagePath(tmp), .configuration("release")) + } + + @Test func testMake() async throws { + var config = sys.make().install().config() + #expect(String(describing: config) == "make install") + } + + @Test func testStrip() async throws { + var config = sys.strip(names: FilePath("foo")).config() + #expect(String(describing: config) == "strip foo") + } + + @Test func testSha256Sum() async throws { + var config = sys.sha256sum(files: FilePath("abcde")).config() + #expect(String(describing: config) == "sha256sum abcde") + } + + @Test func testProductBuild() async throws { + var config = sys.productbuild().synthesize(package: FilePath("mypkg"), distributionOutputPath: FilePath("distribution")).config() + #expect(String(describing: config) == "productbuild --synthesize --package mypkg distribution") + + config = sys.productbuild().distribution(distPath: FilePath("mydist"), productOutputPath: FilePath("product")).config() + #expect(String(describing: config) == "productbuild --distribution mydist product") + + config = sys.productbuild().distribution(.packagePath(FilePath("pkgpath")), .sign("mycert"), distPath: FilePath("mydist"), productOutputPath: FilePath("myproduct")).config() + #expect(String(describing: config) == "productbuild --distribution mydist --package-path pkgpath --sign mycert myproduct") + } + + @Test func testGpg() async throws { + var config = sys.gpg()._import(keys: FilePath("somekeys.asc")).config() + #expect(String(describing: config) == "gpg --import somekeys.asc") + + config = sys.gpg().verify(detachedSignature: FilePath("file.sig"), signedData: FilePath("file")).config() + #expect(String(describing: config) == "gpg --verify file.sig file") + } + + @Test func testPkgutil() async throws { + var config = sys.pkgutil(.verbose).checkSignature(pkgPath: FilePath("path/to/my.pkg")).config() + #expect(String(describing: config) == "pkgutil --verbose --check-signature path/to/my.pkg") + + config = sys.pkgutil(.verbose).expand(pkgPath: FilePath("path/to/my.pkg"), dirPath: FilePath("expand/to/here")).config() + #expect(String(describing: config) == "pkgutil --verbose --expand path/to/my.pkg expand/to/here") + + config = sys.pkgutil(.volume("/Users/foo")).forget(packageId: "com.example.pkg").config() + #expect(String(describing: config) == "pkgutil --volume /Users/foo --forget com.example.pkg") + } + + @Test func testInstaller() async throws { + var config = sys.installer(.verbose, pkg: FilePath("path/to/my.pkg"), target: "CurrentUserHomeDirectory").config() + #expect(String(describing: config) == "installer -verbose -pkg path/to/my.pkg -target CurrentUserHomeDirectory") + } } diff --git a/Tests/SwiftlyTests/HTTPClientTests.swift b/Tests/SwiftlyTests/HTTPClientTests.swift index 96a0472d..db57fa7b 100644 --- a/Tests/SwiftlyTests/HTTPClientTests.swift +++ b/Tests/SwiftlyTests/HTTPClientTests.swift @@ -19,7 +19,7 @@ import Testing } try await withGpg { runGpg in - try runGpg(["--import", "\(tmpFile)"]) + try await runGpg(sys.gpg()._import(keys: tmpFile)) } } } @@ -47,8 +47,8 @@ import Testing try await withGpg { runGpg in try await httpClient.getGpgKeys().download(to: keysFile) - try runGpg(["--import", "\(keysFile)"]) - try runGpg(["--verify", "\(tmpFileSignature)", "\(tmpFile)"]) + try await runGpg(sys.gpg()._import(keys: keysFile)) + try await runGpg(sys.gpg().verify(detachedSignature: tmpFileSignature, signedData: tmpFile)) } } } @@ -76,8 +76,8 @@ import Testing try await withGpg { runGpg in try await httpClient.getGpgKeys().download(to: keysFile) - try runGpg(["--import", "\(keysFile)"]) - try runGpg(["--verify", "\(tmpFileSignature)", "\(tmpFile)"]) + try await runGpg(sys.gpg()._import(keys: keysFile)) + try await runGpg(sys.gpg().verify(detachedSignature: tmpFileSignature, signedData: tmpFile)) } } } @@ -126,15 +126,17 @@ import Testing } } -private func withGpg(_ body: (([String]) throws -> Void) async throws -> Void) async throws { +private func withGpg(_ body: ((Runnable) async throws -> Void) async throws -> Void) async throws { #if os(Linux) // With linux, we can ask gpg to try an import to see if the file is valid // in a sandbox home directory to avoid contaminating the system let gpgHome = fs.mktemp() try await fs.mkdir(.parents, atPath: gpgHome) try await fs.withTemporary(files: gpgHome) { - func runGpg(arguments: [String]) throws { - try Swiftly.currentPlatform.runProgram(["gpg"] + arguments, quiet: false, env: ["GNUPGHOME": gpgHome.string]) + func runGpg(_ runnable: Runnable) async throws { + var env = ProcessInfo.processInfo.environment + env["GNUPGHOME"] = gpgHome.string + try await runnable.run(Swiftly.currentPlatform, env: env, quiet: false) } try await body(runGpg) diff --git a/Tools/build-swiftly-release/BuildSwiftlyRelease.swift b/Tools/build-swiftly-release/BuildSwiftlyRelease.swift index 5b3c472f..cb8cbcdb 100644 --- a/Tools/build-swiftly-release/BuildSwiftlyRelease.swift +++ b/Tools/build-swiftly-release/BuildSwiftlyRelease.swift @@ -213,30 +213,7 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { return nil } - func checkSwiftRequirement() async throws -> String { - guard !self.skip else { - return try await self.assertTool("swift", message: "Please install swift and make sure that it is added to your path.") - } - - guard var requiredSwiftVersion = try? self.findSwiftVersion() else { - throw Error(message: "Unable to determine the required swift version for this version of swiftly. Please make sure that you `cd ` and there is a .swift-version file there.") - } - - if requiredSwiftVersion.hasSuffix(".0") { - requiredSwiftVersion = String(requiredSwiftVersion.dropLast(2)) - } - - let swift = try await self.assertTool("swift", message: "Please install swift \(requiredSwiftVersion) and make sure that it is added to your path.") - - // We also need a swift toolchain with the correct version - guard let swiftVersion = try await runProgramOutput(swift, "--version"), swiftVersion.contains("Swift version \(requiredSwiftVersion)") else { - throw Error(message: "Swiftly releases require a Swift \(requiredSwiftVersion) toolchain available on the path") - } - - return swift - } - - func checkGitRepoStatus(_: String) async throws { + func checkGitRepoStatus() async throws { guard !self.skip else { return } @@ -264,18 +241,11 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { func buildLinuxRelease() async throws { // TODO: turn these into checks that the system meets the criteria for being capable of using the toolchain + checking for packages, not tools let curl = try await self.assertTool("curl", message: "Please install curl with `yum install curl`") - let tar = try await self.assertTool("tar", message: "Please install tar with `yum install tar`") - let make = try await self.assertTool("make", message: "Please install make with `yum install make`") - let git = try await self.assertTool("git", message: "Please install git with `yum install git`") - let strip = try await self.assertTool("strip", message: "Please install strip with `yum install binutils`") - let sha256sum = try await self.assertTool("sha256sum", message: "Please install sha256sum with `yum install coreutils`") - - let swift = try await self.checkSwiftRequirement() - try await self.checkGitRepoStatus(git) + try await self.checkGitRepoStatus() // Start with a fresh SwiftPM package - try runProgram(swift, "package", "reset") + try await sys.swift().package().reset().run(currentPlatform) // Build a specific version of libarchive with a check on the tarball's SHA256 let libArchiveVersion = "3.7.9" @@ -290,19 +260,19 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { try? FileManager.default.removeItem(atPath: libArchivePath) try runProgram(curl, "-L", "-o", "\(buildCheckoutsDir + "/libarchive-\(libArchiveVersion).tar.gz")", "--remote-name", "--location", "https://github.com/libarchive/libarchive/releases/download/v\(libArchiveVersion)/libarchive-\(libArchiveVersion).tar.gz") - let libArchiveTarShaActual = try await runProgramOutput(sha256sum, "\(buildCheckoutsDir)/libarchive-\(libArchiveVersion).tar.gz") + let libArchiveTarShaActual = try await sys.sha256sum(files: FilePath("\(buildCheckoutsDir)/libarchive-\(libArchiveVersion).tar.gz")).output(currentPlatform) guard let libArchiveTarShaActual, libArchiveTarShaActual.starts(with: libArchiveTarSha) else { let shaActual = libArchiveTarShaActual ?? "none" throw Error(message: "The libarchive tar.gz file sha256sum is \(shaActual), but expected \(libArchiveTarSha)") } - try runProgram(tar, "--directory=\(buildCheckoutsDir)", "-xzf", "\(buildCheckoutsDir)/libarchive-\(libArchiveVersion).tar.gz") + try await sys.tar(.directory(FilePath(buildCheckoutsDir))).extract(.compressed, .archive(FilePath("\(buildCheckoutsDir)/libarchive-\(libArchiveVersion).tar.gz"))).run(currentPlatform) let cwd = FileManager.default.currentDirectoryPath FileManager.default.changeCurrentDirectoryPath(libArchivePath) let swiftVerRegex: Regex<(Substring, Substring)> = try! Regex("Swift version (\\d+\\.\\d+\\.?\\d*) ") - let swiftVerOutput = (try await runProgramOutput(swift, "--version")) ?? "" + let swiftVerOutput = (try await runProgramOutput("swift", "--version")) ?? "" guard let swiftVerMatch = try swiftVerRegex.firstMatch(in: swiftVerOutput) else { throw Error(message: "Unable to detect swift version") } @@ -328,7 +298,7 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { throw Error(message: "Swift release \(swiftVersion) has no Static SDK offering") } - try runProgram(swift, "sdk", "install", "https://download.swift.org/swift-\(swiftVersion)-release/static-sdk/swift-\(swiftVersion)-RELEASE/swift-\(swiftVersion)-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz", "--checksum", sdkPlatform.checksum ?? "deadbeef") + try await sys.swift().sdk().install("https://download.swift.org/swift-\(swiftVersion)-release/static-sdk/swift-\(swiftVersion)-RELEASE/swift-\(swiftVersion)-RELEASE_static-linux-0.0.1.artifactbundle.tar.gz", checksum: sdkPlatform.checksum ?? "deadbeef").run(currentPlatform) var customEnv = ProcessInfo.processInfo.environment customEnv["CC"] = "\(cwd)/Tools/build-swiftly-release/musl-clang" @@ -356,18 +326,18 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { env: customEnv ) - try runProgramEnv(make, env: customEnv) + try await sys.make().run(currentPlatform, env: customEnv) - try runProgram(make, "install") + try await sys.make().install().run(currentPlatform) FileManager.default.changeCurrentDirectoryPath(cwd) - try runProgram(swift, "build", "--swift-sdk", "\(arch)-swift-linux-musl", "--product=swiftly", "--pkg-config-path=\(pkgConfigPath)/lib/pkgconfig", "--static-swift-stdlib", "--configuration=release") + try await sys.swift().build(.swiftSdk("\(arch)-swift-linux-musl"), .product("swiftly"), .pkgConfigPath("\(pkgConfigPath)/lib/pkgconfig"), .staticSwiftStdlib, .configuration("release")).run(currentPlatform) let releaseDir = cwd + "/.build/release" // Strip the symbols from the binary to decrease its size - try runProgram(strip, releaseDir + "/swiftly") + try await sys.strip(names: FilePath(releaseDir) / "swiftly").run(currentPlatform) try await self.collectLicenses(releaseDir) @@ -377,7 +347,7 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { let releaseArchive = "\(releaseDir)/swiftly-\(version)-x86_64.tar.gz" #endif - try runProgram(tar, "--directory=\(releaseDir)", "-czf", releaseArchive, "swiftly", "LICENSE.txt") + try await sys.tar(.directory(FilePath(releaseDir))).create(.compressed, .archive(FilePath(releaseArchive)), files: "swiftly", "LICENSE.txt").run(currentPlatform) print(releaseArchive) @@ -390,32 +360,23 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { let testArchive = "\(debugDir)/test-swiftly-linux-x86_64.tar.gz" #endif - try runProgram(swift, "build", "--swift-sdk", "\(arch)-swift-linux-musl", "--product=test-swiftly", "--pkg-config-path=\(pkgConfigPath)/lib/pkgconfig", "--static-swift-stdlib", "--configuration=debug") - try runProgram(tar, "--directory=\(debugDir)", "-czf", testArchive, "test-swiftly") + try await sys.swift().build(.swiftSdk("\(arch)-swift-linux-musl"), .product("test-swiftly"), .pkgConfigPath("\(pkgConfigPath)/lib/pkgconfig"), .staticSwiftStdlib, .configuration("debug")).run(currentPlatform) + try await sys.tar(.directory(FilePath(debugDir))).create(.compressed, .archive(FilePath(testArchive)), files: "test-swiftly").run(currentPlatform) print(testArchive) } - try runProgram(swift, "sdk", "remove", sdkName) + try await sys.swift().sdk().remove(sdkName).runEcho(currentPlatform) } func buildMacOSRelease(cert: String?, identifier: String) async throws { - // Check system requirements - let git = try await self.assertTool("git", message: "Please install git with either `xcode-select --install` or `brew install git`") - - let swift = try await checkSwiftRequirement() - - try await self.checkGitRepoStatus(git) - - let strip = try await self.assertTool("strip", message: "In order to strip binaries there needs to be the `strip` tool that is installed on macOS.") - - let tar = try await self.assertTool("tar", message: "In order to produce archives there needs to be the `tar` tool that is installed on macOS.") + try await self.checkGitRepoStatus() - try runProgram(swift, "package", "clean") + try await sys.swift().package().clean().run(currentPlatform) for arch in ["x86_64", "arm64"] { - try runProgram(swift, "build", "--product=swiftly", "--configuration=release", "--arch=\(arch)") - try runProgram(strip, ".build/\(arch)-apple-macosx/release/swiftly") + try await sys.swift().build(.product("swiftly"), .configuration("release"), .arch("\(arch)")).run(currentPlatform) + try await sys.strip(names: FilePath(".build") / "\(arch)-apple-macosx/release/swiftly").run(currentPlatform) } let swiftlyBinDir = fs.cwd / ".build/release/.swiftly/bin" @@ -461,16 +422,16 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { let pkgFileReconfigured = releaseDir.appendingPathComponent("swiftly-\(self.version)-reconfigured.pkg") let distFile = releaseDir.appendingPathComponent("distribution.plist") - try runProgram("productbuild", "--synthesize", "--package", pkgFile.path, distFile.path) + try await sys.productbuild().synthesize(package: FilePath(pkgFile.path), distributionOutputPath: FilePath(distFile.path)).runEcho(currentPlatform) var distFileContents = try String(contentsOf: distFile, encoding: .utf8) distFileContents = distFileContents.replacingOccurrences(of: "", with: "swiftly") try distFileContents.write(to: distFile, atomically: true, encoding: .utf8) if let cert = cert { - try runProgram("productbuild", "--distribution", distFile.path, "--package-path", pkgFile.deletingLastPathComponent().path, "--sign", cert, pkgFileReconfigured.path) + try await sys.productbuild().distribution(.packagePath(FilePath(pkgFile.deletingLastPathComponent().path)), .sign(cert), distPath: FilePath(distFile.path), productOutputPath: FilePath(pkgFileReconfigured.path)).runEcho(currentPlatform) } else { - try runProgram("productbuild", "--distribution", distFile.path, "--package-path", pkgFile.deletingLastPathComponent().path, pkgFileReconfigured.path) + try await sys.productbuild().distribution(.packagePath(FilePath(pkgFile.deletingLastPathComponent().path)), distPath: FilePath(distFile.path), productOutputPath: FilePath(pkgFileReconfigured.path)).runEcho(currentPlatform) } try FileManager.default.removeItem(at: pkgFile) try FileManager.default.copyItem(atPath: pkgFileReconfigured.path, toPath: pkgFile.path) @@ -479,8 +440,8 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { if self.test { for arch in ["x86_64", "arm64"] { - try runProgram(swift, "build", "--product=test-swiftly", "--configuration=debug", "--arch=\(arch)") - try runProgram(strip, ".build/\(arch)-apple-macosx/release/swiftly") + try await sys.swift().build(.product("test-swiftly"), .configuration("debug"), .arch("\(arch)")).runEcho(currentPlatform) + try await sys.strip(names: FilePath(".build") / "\(arch)-apple-macosx/release/swiftly").runEcho(currentPlatform) } let testArchive = releaseDir.appendingPathComponent("test-swiftly-macos.tar.gz") @@ -491,7 +452,7 @@ struct BuildSwiftlyRelease: AsyncParsableCommand { .create(output: swiftlyBinDir / "swiftly") .runEcho(currentPlatform) - try runProgram(tar, "--directory=.build/x86_64-apple-macosx/debug", "-czf", testArchive.path, "test-swiftly") + try await sys.tar(.directory(FilePath(".build/x86_64-apple-macosx/debug"))).create(.compressed, .archive(FilePath(testArchive.path)), files: "test-swiftly").run(currentPlatform) print(testArchive.path) }