Nucleic: Gitea Runner macOS VM Support
This commit is contained in:
@@ -0,0 +1,110 @@
|
||||
import Foundation
|
||||
|
||||
#if canImport(Darwin)
|
||||
import Darwin
|
||||
#elseif canImport(Glibc)
|
||||
import Glibc
|
||||
#endif
|
||||
|
||||
/// Resolves a path typed on the command line into a single concrete path.
|
||||
///
|
||||
/// The problem this exists for: an operator writes
|
||||
/// `--ipsw ~/Downloads/UniversalMac_27.0_*.ipsw`. Unquoted, the shell may expand
|
||||
/// the glob before we ever run — or, in `zsh`, refuse to run the command at all
|
||||
/// with `no matches found`. Quoted, we receive the pattern verbatim, tilde and
|
||||
/// asterisk included, and a plain `expandingTildeInPath` leaves a `*` sitting in
|
||||
/// the middle of a path that will never exist. Either way the operator sees a
|
||||
/// tool that "only works with quotes" (or only without them).
|
||||
///
|
||||
/// So the CLI resolves the argument itself and stops depending on which shell
|
||||
/// ran it. This is deliberately a **CLI-argument affordance only**: values read
|
||||
/// out of `config.json` get tilde expansion (see
|
||||
/// ``RunnerConfig/expandTilde(_:)``) and nothing more, because a config file is
|
||||
/// not typed at a prompt and a stray `*` there is a mistake, not a pattern.
|
||||
public enum PathResolution {
|
||||
|
||||
/// Characters that make a string a pattern rather than a path.
|
||||
///
|
||||
/// `~` is not one of them: it is expanded unconditionally, pattern or not.
|
||||
private static let metacharacters: Set<Character> = ["*", "?", "["]
|
||||
|
||||
/// Whether `path` should be treated as a glob pattern.
|
||||
public static func isPattern(_ path: String) -> Bool {
|
||||
path.contains(where: metacharacters.contains)
|
||||
}
|
||||
|
||||
/// Expands a leading `~`, then resolves any glob pattern to one path.
|
||||
///
|
||||
/// A string with no metacharacters passes through with only tilde
|
||||
/// expansion — in particular it is *not* checked for existence, so the
|
||||
/// caller's own error (which knows what the file was for) is what an
|
||||
/// operator sees for an ordinary typo.
|
||||
///
|
||||
/// A string that does contain metacharacters is matched with `glob(3)`. If
|
||||
/// it matches nothing but a file exists at that literal name, the literal
|
||||
/// wins: `report[1].ipsw` is a legal filename, and only failing to match
|
||||
/// tells us it was meant as one rather than as a character class.
|
||||
///
|
||||
/// - Parameters:
|
||||
/// - path: The raw argument, as typed.
|
||||
/// - label: What the argument names, for error messages (`"--ipsw"`).
|
||||
/// - Returns: A single concrete path.
|
||||
/// - Throws: ``CoreError/notFound(_:)`` when a pattern matches nothing, or
|
||||
/// ``CoreError/configInvalid(_:)`` when it matches more than one file —
|
||||
/// picking one arbitrarily would silently build the wrong image.
|
||||
public static func resolve(_ path: String, label: String) throws -> String {
|
||||
let expanded = RunnerConfig.expandTilde(path)
|
||||
guard isPattern(expanded) else { return expanded }
|
||||
|
||||
let matches = glob(pattern: expanded)
|
||||
|
||||
switch matches.count {
|
||||
case 1:
|
||||
return matches[0]
|
||||
case 0:
|
||||
if FileManager.default.fileExists(atPath: expanded) { return expanded }
|
||||
throw CoreError.notFound("\(label): no file matches \(expanded)")
|
||||
default:
|
||||
let listed = matches.map { " \($0)" }.joined(separator: "\n")
|
||||
throw CoreError.configInvalid(
|
||||
"\(label): \(matches.count) files match \(expanded):\n\(listed)\n"
|
||||
+ "name exactly one of them"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// All paths matching `pattern`, sorted.
|
||||
///
|
||||
/// Sorted explicitly rather than relying on `glob(3)`'s own ordering, which
|
||||
/// is locale-dependent — the error message above lists these, and a listing
|
||||
/// that reorders between runs is a poor thing to ask someone to read.
|
||||
static func glob(pattern: String) -> [String] {
|
||||
var result = glob_t()
|
||||
defer { globfree(&result) }
|
||||
|
||||
guard Glibc_glob(pattern, &result) == 0 else { return [] }
|
||||
guard let paths = result.gl_pathv else { return [] }
|
||||
|
||||
var found: [String] = []
|
||||
for index in 0..<Int(result.gl_pathc) {
|
||||
guard let entry = paths[index] else { continue }
|
||||
found.append(String(cString: entry))
|
||||
}
|
||||
return found.sorted()
|
||||
}
|
||||
|
||||
/// Thin shim so the call above reads the same on both platforms; `glob(3)`
|
||||
/// is otherwise shadowed by the ``glob(pattern:)`` above.
|
||||
private static func Glibc_glob(
|
||||
_ pattern: String,
|
||||
_ result: UnsafeMutablePointer<glob_t>
|
||||
) -> Int32 {
|
||||
#if canImport(Darwin)
|
||||
return Darwin.glob(pattern, 0, nil, result)
|
||||
#elseif canImport(Glibc)
|
||||
return Glibc.glob(pattern, 0, nil, result)
|
||||
#else
|
||||
return -1
|
||||
#endif
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user