Skip to content

Model additional system commands #332

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

Merged
merged 4 commits into from
Apr 30, 2025
Merged
Show file tree
Hide file tree
Changes from 3 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
12 changes: 3 additions & 9 deletions Sources/LinuxPlatform/Linux.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import Foundation
import SwiftlyCore
import SystemPackage

typealias sys = SwiftlyCore.SystemCommand
typealias fs = SwiftlyCore.FileSystem

/// `Platform` implementation for Linux systems.
Expand Down Expand Up @@ -605,15 +606,8 @@ public struct Linux: Platform {

public func getShell() async throws -> String {
let userName = ProcessInfo.processInfo.userName
let prefix = "\(userName):"
if let passwds = try await runProgramOutput("getent", "passwd") {
for line in passwds.components(separatedBy: "\n") {
if line.hasPrefix(prefix) {
if case let comps = line.components(separatedBy: ":"), comps.count > 1 {
return comps[comps.count - 1]
}
}
}
if let entry = try await sys.getent(database: "passwd", keys: userName).entries(self).first {
if let shell = entry.last { return shell }
}

// Fall back on bash on Linux and other Unixes
Expand Down
335 changes: 334 additions & 1 deletion Sources/SwiftlyCore/Commands.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,8 @@ extension SystemCommand.DsclCommand.ReadCommand: Output {
}
}

// Create or operate on universal files
// See lipo(1) for details
extension SystemCommand {
public static func lipo(executable: Executable = LipoCommand.defaultExecutable, inputFiles: FilePath...) -> LipoCommand {
Self.lipo(executable: executable, inputFiles: inputFiles)
Expand All @@ -106,7 +108,7 @@ extension SystemCommand {
var executable: Executable
var inputFiles: [FilePath]

public init(executable: Executable, inputFiles: [FilePath]) {
internal init(executable: Executable, inputFiles: [FilePath]) {
self.executable = executable
self.inputFiles = inputFiles
}
Expand Down Expand Up @@ -150,3 +152,334 @@ 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 {
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)
}

public static func pkgbuild(executable: Executable = PkgbuildCommand.defaultExecutable, options: [PkgbuildCommand.Option], root: FilePath, packageOutputPath: FilePath) -> PkgbuildCommand {
PkgbuildCommand(executable: executable, options, root: root, packageOutputPath: packageOutputPath)
}

public struct PkgbuildCommand {
public static var defaultExecutable: Executable { .name("pkgbuild") }

var executable: Executable

var options: [Option]

var root: FilePath
var packageOutputPath: FilePath

internal init(executable: Executable, _ options: [Option], root: FilePath, packageOutputPath: FilePath) {
self.executable = executable
self.options = options
self.root = root
self.packageOutputPath = packageOutputPath
}

public enum Option {
case installLocation(FilePath)
case version(String)
case identifier(String)
case sign(String)

func args() -> [String] {
switch self {
case let .installLocation(installLocation):
return ["--install-location", installLocation.string]
case let .version(version):
return ["--version", version]
case let .identifier(identifier):
return ["--identifier", identifier]
case let .sign(identityName):
return ["--sign", identityName]
}
}
}

public func config() -> Configuration {
var args: [String] = []

for option in self.options {
args += option.args()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: favour .append(contentsOf:) over += here, as .append is a more specialized implementation that won't create an intermediate array.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks, I've replaced them all with an equivalent append method.

}

args += ["--root", "\(self.root)"]
args += ["\(self.packageOutputPath)"]

return Configuration(
executable: self.executable,
arguments: Arguments(args),
environment: .inherit
)
}
}
}

extension SystemCommand.PkgbuildCommand: Runnable {}

// get entries from Name Service Switch libraries
// See getent(1) for more details
extension SystemCommand {
public static func getent(executable: Executable = GetentCommand.defaultExecutable, database: String, keys: String...) -> GetentCommand {
Self.getent(executable: executable, database: database, keys: keys)
}

public static func getent(executable: Executable = GetentCommand.defaultExecutable, database: String, keys: [String]) -> GetentCommand {
GetentCommand(executable: executable, database: database, keys: keys)
}

public struct GetentCommand {
public static var defaultExecutable: Executable { .name("getent") }

var executable: Executable

var database: String

var keys: [String]

internal init(
executable: Executable,
database: String,
keys: [String]
) {
self.executable = executable
self.database = database
self.keys = keys
}

public func config() -> Configuration {
var args: [String] = []

args += [self.database]
args += self.keys

return Configuration(
executable: self.executable,
arguments: Arguments(args),
environment: .inherit
)
}
}
}

extension SystemCommand.GetentCommand: Output {
public func entries(_ platform: Platform) async throws -> [[String]] {
let output = try await output(platform)
guard let output else { return [] }

var entries: [[String]] = []
for line in output.components(separatedBy: "\n") {
entries.append(line.components(separatedBy: ":"))
}
return entries
}
}

extension SystemCommand {
public static func git(executable: Executable = GitCommand.defaultExecutable, workingDir: FilePath? = nil) -> GitCommand {
GitCommand(executable: executable, workingDir: workingDir)
}

public struct GitCommand {
public static var defaultExecutable: Executable { .name("git") }

var executable: Executable

var workingDir: FilePath?

internal init(executable: Executable, workingDir: FilePath?) {
self.executable = executable
self.workingDir = workingDir
}

func config() -> Configuration {
var args: [String] = []

if let workingDir {
args += ["-C", "\(workingDir)"]
}

return Configuration(
executable: self.executable,
arguments: Arguments(args),
environment: .inherit
)
}

public func _init() -> InitCommand {
InitCommand(self)
}

public struct InitCommand {
var git: GitCommand

internal init(_ git: GitCommand) {
self.git = git
}

public func config() -> Configuration {
var c = self.git.config()

var args = c.arguments.storage.map(\.description)

args += ["init"]

c.arguments = .init(args)

return c
}
}

public func commit(_ options: CommitCommand.Option...) -> CommitCommand {
self.commit(options: options)
}

public func commit(options: [CommitCommand.Option]) -> CommitCommand {
CommitCommand(self, options: options)
}

public struct CommitCommand {
var git: GitCommand

var options: [Option]

internal init(_ git: GitCommand, options: [Option]) {
self.git = git
self.options = options
}

public enum Option {
case allowEmpty
case allowEmptyMessage
case message(String)

public func args() -> [String] {
switch self {
case .allowEmpty:
["--allow-empty"]
case .allowEmptyMessage:
["--allow-empty-message"]
case let .message(message):
["-m", message]
}
}
}

public func config() -> Configuration {
var c = self.git.config()

var args = c.arguments.storage.map(\.description)

args += ["commit"]
for option in self.options {
args += option.args()
}

c.arguments = .init(args)

return c
}
}

public func log(_ options: LogCommand.Option...) -> LogCommand {
LogCommand(self, options)
}

public struct LogCommand {
var git: GitCommand
var options: [Option]

internal init(_ git: GitCommand, _ options: [Option]) {
self.git = git
self.options = options
}

public enum Option {
case maxCount(Int)
case pretty(String)

func args() -> [String] {
switch self {
case let .maxCount(num):
return ["--max-count=\(num)"]
case let .pretty(format):
return ["--pretty=\(format)"]
}
}
}

public func config() -> Configuration {
var c = self.git.config()

var args = c.arguments.storage.map(\.description)

args += ["log"]

for opt in self.options {
args += opt.args()
}

c.arguments = .init(args)

return c
}
}

public func diffIndex(_ options: DiffIndexCommand.Option..., treeIsh: String?) -> DiffIndexCommand {
DiffIndexCommand(self, options, treeIsh: treeIsh)
}

public struct DiffIndexCommand {
var git: GitCommand
var options: [Option]
var treeIsh: String?

internal init(_ git: GitCommand, _ options: [Option], treeIsh: String?) {
self.git = git
self.options = options
self.treeIsh = treeIsh
}

public enum Option {
case quiet

func args() -> [String] {
switch self {
case .quiet:
return ["--quiet"]
}
}
}

public func config() -> Configuration {
var c = self.git.config()

var args = c.arguments.storage.map(\.description)

args += ["diff-index"]

for opt in self.options {
args += opt.args()
}

if let treeIsh = self.treeIsh {
args += [treeIsh]
}

c.arguments = .init(args)

return c
}
}
}
}

extension SystemCommand.GitCommand.LogCommand: Output {}
extension SystemCommand.GitCommand.DiffIndexCommand: Runnable {}
extension SystemCommand.GitCommand.InitCommand: Runnable {}
extension SystemCommand.GitCommand.CommitCommand: Runnable {}
Loading