Compare commits

..

4 Commits

Author SHA1 Message Date
T. R. Bernstein
6927464d47 Use Subprocess instead of Shwift
Some checks failed
Docs / docs (push) Has been cancelled
Docs / deploy (push) Has been cancelled
Drop Shwift: it is incompatible with musl (used by the Swift static
linking SDK), and its API is not meaningfully more concise than
Subprocess upon closer inspection.
2026-03-23 19:50:58 +01:00
T. R. Bernstein
31ed16c828 Cache build directory of linux containers
SwiftPM uses caches heavily to reduce compilation and download time.
Before this change, we through these caches away with each container.
2026-03-22 17:51:24 +01:00
T. R. Bernstein
ac1c86c431 Run linux container with same architechture as host
As the development team uses both Intel and Apple Silicon Macs,
we have to get the host CPU architecture at compilation time instead of
harcoding it.
If the container has a different architecture, the guest has to be
emulated.
2026-03-22 17:51:24 +01:00
T. R. Bernstein
4b28c293cb Use Shwift library instead of Subprocess
Shwift has a concise API, which makes writing shell code nice and easy.
This is an opinionated decision.
2026-03-22 17:51:05 +01:00
7 changed files with 77 additions and 44 deletions

View File

@@ -1,5 +1,5 @@
{ {
"originHash" : "0cb2e87817f52021ac25ffee6b27396f6d94e9fd604ca83db7f20a10e65fe6cf", "originHash" : "17ce26ba5c862ca674cd3ceeb43a9fe8a5c5251c5561de65e632a06d79916342",
"pins" : [ "pins" : [
{ {
"identity" : "noora", "identity" : "noora",
@@ -28,22 +28,13 @@
"version" : "4.2.1" "version" : "4.2.1"
} }
}, },
{
"identity" : "shwift",
"kind" : "remoteSourceControl",
"location" : "https://github.com/GeorgeLyon/Shwift",
"state" : {
"revision" : "d7be04898d094ddce6140cc6a2e9a83fc994b66d",
"version" : "3.1.1"
}
},
{ {
"identity" : "swift-argument-parser", "identity" : "swift-argument-parser",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser", "location" : "https://github.com/apple/swift-argument-parser",
"state" : { "state" : {
"revision" : "c5d11a805e765f52ba34ec7284bd4fcd6ba68615", "revision" : "626b5b7b2f45e1b0b1c6f4a309296d1d21d7311b",
"version" : "1.7.0" "version" : "1.7.1"
} }
}, },
{ {
@@ -82,6 +73,15 @@
"version" : "2.95.0" "version" : "2.95.0"
} }
}, },
{
"identity" : "swift-subprocess",
"kind" : "remoteSourceControl",
"location" : "https://github.com/swiftlang/swift-subprocess.git",
"state" : {
"revision" : "ba5888ad7758cbcbe7abebac37860b1652af2d9c",
"version" : "0.3.0"
}
},
{ {
"identity" : "swift-system", "identity" : "swift-system",
"kind" : "remoteSourceControl", "kind" : "remoteSourceControl",

View File

@@ -11,10 +11,11 @@ let package = Package(
) )
], ],
dependencies: [ dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.7.1"),
.package(url: "https://github.com/apple/swift-log", from: "1.10.1"), .package(url: "https://github.com/apple/swift-log", from: "1.10.1"),
.package(url: "https://github.com/apple/swift-nio", from: "2.95.0"), .package(url: "https://github.com/apple/swift-nio", from: "2.95.0"),
.package(url: "https://github.com/apple/swift-system", from: "1.6.4"), .package(url: "https://github.com/apple/swift-system", from: "1.6.4"),
.package(url: "https://github.com/GeorgeLyon/Shwift", from: "3.1.1"), .package(url: "https://github.com/swiftlang/swift-subprocess.git", from: "0.3.0"),
.package(url: "https://github.com/tuist/Noora", from: "0.55.1") .package(url: "https://github.com/tuist/Noora", from: "0.55.1")
], ],
targets: [ targets: [
@@ -38,9 +39,10 @@ let package = Package(
.executableTarget( .executableTarget(
name: "InotifyTaskCLI", name: "InotifyTaskCLI",
dependencies: [ dependencies: [
.product(name: "ArgumentParser", package: "swift-argument-parser"),
.product(name: "Logging", package: "swift-log"), .product(name: "Logging", package: "swift-log"),
.product(name: "_NIOFileSystem", package: "swift-nio"), .product(name: "_NIOFileSystem", package: "swift-nio"),
.product(name: "Script", package: "Shwift"), .product(name: "Subprocess", package: "swift-subprocess"),
.product(name: "Noora", package: "Noora") .product(name: "Noora", package: "Noora")
], ],
path: "Sources/TaskCLI" path: "Sources/TaskCLI"

View File

@@ -1,4 +1,4 @@
import Script import ArgumentParser
@main @main
struct Command: AsyncParsableCommand { struct Command: AsyncParsableCommand {

View File

@@ -0,0 +1,9 @@
struct Docker {
static func getLinuxPlatformStringWithHostArchitecture() -> String {
#if arch(x86_64)
return "linux/amd64"
#else
return "linux/arm64"
#endif
}
}

View File

@@ -1,9 +1,10 @@
import ArgumentParser
import Foundation import Foundation
import Logging import Logging
import Script
import Noora import Noora
import Subprocess
struct GenerateDocumentationCommand: Script { struct GenerateDocumentationCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration( static let configuration = CommandConfiguration(
commandName: "generate-documentation", commandName: "generate-documentation",
abstract: "Generate DocC documentation of all targets inside a Linux container.", abstract: "Generate DocC documentation of all targets inside a Linux container.",
@@ -23,7 +24,6 @@ struct GenerateDocumentationCommand: Script {
let logger = global.makeLogger(labeled: "swift-inotify.cli.task.generate-documentation") let logger = global.makeLogger(labeled: "swift-inotify.cli.task.generate-documentation")
let fileManager = FileManager.default let fileManager = FileManager.default
let projectDirectory = URL(fileURLWithPath: fileManager.currentDirectoryPath) let projectDirectory = URL(fileURLWithPath: fileManager.currentDirectoryPath)
let docker = try await executable(named: "docker")
let targets = try await Self.targets(for: projectDirectory) let targets = try await Self.targets(for: projectDirectory)
@@ -42,16 +42,21 @@ struct GenerateDocumentationCommand: Script {
let script = Self.makeRunScript(for: targets) let script = Self.makeRunScript(for: targets)
logger.debug("Container script", metadata: ["script": "\(script)"]) logger.debug("Container script", metadata: ["script": "\(script)"])
do { let dockerRunResult = try await Subprocess.run(
try await docker( .name("docker"),
arguments: [
"run", "--rm", "run", "--rm",
"-v", "\(tempDirectory.path):/code", "-v", "\(tempDirectory.path):/code",
"--platform", "linux/arm64", "-v", "swift-inotify-build-cache:/code/.build",
"--platform", Docker.getLinuxPlatformStringWithHostArchitecture(),
"-w", "/code", "-w", "/code",
"swift:latest", "swift:latest",
"/bin/bash", "-c", script, "/bin/bash", "-c", script
) ],
} catch { output: .standardOutput,
error: .standardError
)
if !dockerRunResult.terminationStatus.isSuccess {
noora.error("Documentation generation failed.") noora.error("Documentation generation failed.")
return return
} }
@@ -103,10 +108,12 @@ struct GenerateDocumentationCommand: Script {
} }
private static func packageTargets() async throws -> [(name: String, path: String)] { private static func packageTargets() async throws -> [(name: String, path: String)] {
let swift = try await executable(named: "swift") let packageDescriptionResult = try await Subprocess.run(
let packageDescriptionOutput = try await outputOf { .name("swift"),
try await swift("package", "describe", "--type", "json") arguments: ["package", "describe", "--type", "json"],
} output: .data(limit: 10_000),
error: .standardError
)
struct PackageDescription: Codable { struct PackageDescription: Codable {
let targets: [Target] let targets: [Target]
@@ -116,8 +123,11 @@ struct GenerateDocumentationCommand: Script {
let path: String let path: String
} }
let data = Data(packageDescriptionOutput.utf8) if !packageDescriptionResult.terminationStatus.isSuccess {
let package = try JSONDecoder().decode(PackageDescription.self, from: data) throw GenerateDocumentationError.unableToReadPackageDescription
}
let package = try JSONDecoder().decode(PackageDescription.self, from: packageDescriptionResult.standardOutput)
return package.targets.map { ($0.name, $0.path) } return package.targets.map { ($0.name, $0.path) }
} }
@@ -162,13 +172,16 @@ struct GenerateDocumentationCommand: Script {
// MARK: - Dependency Injection // MARK: - Dependency Injection
private func injectDoccPluginDependency(in directory: URL, logger: Logger) async throws { private func injectDoccPluginDependency(in directory: URL, logger: Logger) async throws {
let swift = try await executable(named: "swift") let swiftRunResult = try await Subprocess.run(
do { .name("swift"),
try await swift( arguments: [
"package", "--package-path", directory.path(percentEncoded: false), "package", "--package-path", directory.path(percentEncoded: false),
"add-dependency", "--from", Self.doccPluginMinVersion, Self.doccPluginURL "add-dependency", "--from", Self.doccPluginMinVersion, Self.doccPluginURL
) ],
} catch { output: .standardOutput,
error: .standardError
)
if !swiftRunResult.terminationStatus.isSuccess {
throw GenerateDocumentationError.dependencyInjectionFailed throw GenerateDocumentationError.dependencyInjectionFailed
} }
@@ -178,11 +191,14 @@ struct GenerateDocumentationCommand: Script {
enum GenerateDocumentationError: Error, CustomStringConvertible { enum GenerateDocumentationError: Error, CustomStringConvertible {
case dependencyInjectionFailed case dependencyInjectionFailed
case unableToReadPackageDescription
var description: String { var description: String {
switch self { switch self {
case .dependencyInjectionFailed: case .dependencyInjectionFailed:
"Failed to add swift-docc-plugin dependency to Package.swift." "Failed to add swift-docc-plugin dependency to Package.swift."
case .unableToReadPackageDescription:
"Failed to read the package description."
} }
} }
} }

View File

@@ -1,4 +1,4 @@
import Script import ArgumentParser
import Logging import Logging
struct GlobalOptions: ParsableArguments { struct GlobalOptions: ParsableArguments {

View File

@@ -1,8 +1,9 @@
import ArgumentParser
import Foundation import Foundation
import Script
import Noora import Noora
import Subprocess
struct TestCommand: Script { struct TestCommand: AsyncParsableCommand {
static let configuration = CommandConfiguration( static let configuration = CommandConfiguration(
commandName: "test", commandName: "test",
abstract: "Run swift test in a linux container.", abstract: "Run swift test in a linux container.",
@@ -17,21 +18,26 @@ struct TestCommand: Script {
let noora = Noora() let noora = Noora()
let logger = global.makeLogger(labeled: "swift-inotify.cli.task.test") let logger = global.makeLogger(labeled: "swift-inotify.cli.task.test")
let currentDirectory = FileManager.default.currentDirectoryPath let currentDirectory = FileManager.default.currentDirectoryPath
let docker = Executable(path: "/opt/homebrew/bin/docker")
noora.info("Running tests on Linux.") noora.info("Running tests on Linux.")
logger.debug("Current directory", metadata: ["current-directory": "\(currentDirectory)"]) logger.debug("Current directory", metadata: ["current-directory": "\(currentDirectory)"])
do { let dockerRunResult = try await Subprocess.run(
try await docker( .name("docker"),
arguments: [
"run", "run",
"-v", "\(currentDirectory):/code", "-v", "\(currentDirectory):/code",
"-v", "swift-inotify-build-cache:/code/.build",
"--security-opt", "systempaths=unconfined", "--security-opt", "systempaths=unconfined",
"--platform", "linux/arm64", "--platform", Docker.getLinuxPlatformStringWithHostArchitecture(),
"-w", "/code", "swift:latest", "-w", "/code", "swift:latest",
"/bin/bash", "-c", "swift test --skip InotifyLimitTests; swift test --skip-build --filter InotifyLimitTests" "/bin/bash", "-c", "swift test --skip InotifyLimitTests; swift test --skip-build --filter InotifyLimitTests"
) ],
output: .standardOutput,
error: .standardError
)
if dockerRunResult.terminationStatus.isSuccess {
noora.success("All tests completed successfully.") noora.success("All tests completed successfully.")
} catch { } else {
noora.error("Not all tests completed successfully.") noora.error("Not all tests completed successfully.")
} }
} }