Compare commits

...
5 Commits
Author SHA1 Message Date
Nikolay Edigaryev e35c13425e tart exec: handle input redirection of regular files (#1106) 2025-07-14 19:49:12 +04:00
Nikolay Edigaryev 0debec1266 docs: include full article content in RSS (#1104) 2025-07-08 21:06:37 +04:00
Nikolay Edigaryev 294c5fc5e5 Upgrade Swift Argument Parser to 1.6.1 (#1103)
* Upgrade Swift Argument Parser to 1.6.1

* Remove ArgumentParser workaround
2025-07-08 00:19:50 +04:00
Fedor Korotkov 99777b6740 Update README example to use macOS Tahoe (#1101) 2025-07-07 13:50:16 +04:00
Fedor KorotkovandClaude a2972aa4d9 feat: prioritize pruning of old SHA when pulling updated tags (#1102)
* feat: prioritize pruning of old SHA when pulling updated tags

When pulling a new version of a tagged image (e.g., ghcr.io/cirruslabs/macos-runner:sonoma),
set the access date of the previous SHA to epoch time (1970-01-01). This ensures that the
old SHA will be prioritized for pruning, even if it was accessed more recently than other
cached images.

This helps manage disk space more efficiently by automatically cleaning up superseded
versions of frequently-updated tagged images.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* format

* Review comments

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-07-07 08:52:05 +00:00
9 changed files with 70 additions and 29 deletions
+1
View File
@@ -29,6 +29,7 @@ Table of Contents
1. Code should follow camel case
2. Code should follow [SwiftFormat](https://github.com/nicklockwood/SwiftFormat#swift-package-manager-plugin) guidelines. You can auto-format the code by running the following command:
```bash
swift package plugin --allow-writing-to-package-directory swiftformat --cache ignore .
```
+3 -3
View File
@@ -1,5 +1,5 @@
{
"originHash" : "c5371137580239f6928cf64425e754e77680bf24430b50f02dad5558c23f68b0",
"originHash" : "668bad809d4882f75f097e66a12a6dbc8e61ec998f1800a7e09439c854fadda1",
"pins" : [
{
"identity" : "antlr4",
@@ -78,8 +78,8 @@
"kind" : "remoteSourceControl",
"location" : "https://github.com/apple/swift-argument-parser",
"state" : {
"revision" : "41982a3656a71c768319979febd796c6fd111d5c",
"version" : "1.5.0"
"revision" : "309a47b2b1d9b5e991f36961c983ecec72275be3",
"version" : "1.6.1"
}
},
{
+1 -1
View File
@@ -10,7 +10,7 @@ let package = Package(
.executable(name: "tart", targets: ["tart"])
],
dependencies: [
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.3.1"),
.package(url: "https://github.com/apple/swift-argument-parser", from: "1.6.1"),
.package(url: "https://github.com/mhdhejazi/Dynamic", branch: "master"),
.package(url: "https://github.com/apple/swift-algorithms", from: "1.2.0"),
.package(url: "https://github.com/malcommac/SwiftDate", from: "7.0.0"),
+2 -2
View File
@@ -66,8 +66,8 @@ Try running a Tart VM on your Apple Silicon device running macOS 13.0 (Ventura)
```bash
brew install cirruslabs/cli/tart
tart clone ghcr.io/cirruslabs/macos-sequoia-base:latest sequoia-base
tart run sequoia-base
tart clone ghcr.io/cirruslabs/macos-tahoe-base:latest tahoe-base
tart run tahoe-base
```
Please check the [official documentation](https://tart.run) for more information and/or feel free to use [discussions](https://github.com/cirruslabs/tart/discussions)
+51 -8
View File
@@ -100,28 +100,61 @@ struct Exec: AsyncParsableCommand {
try await withThrowingTaskGroup { group in
// Stream host's standard input if interactive mode is enabled
if interactive {
let stdinStream = AsyncStream<Data> { continuation in
let stdinStream = AsyncThrowingStream<Data, Error> { continuation in
let handle = FileHandle.standardInput
handle.readabilityHandler = { handle in
let data = handle.availableData
if isRegularFile(handle.fileDescriptor) {
// Standard input can be a regular file when input redirection (<) is used,
// in which case the handle won't receive any new readability events, so we
// just read the file normally here in chunks and consider done with it
//
// Ideally this is best handled by using non-blocking I/O, but Swift's
// standard library only offers inefficient bytes[1] property and SwiftNIO's
// NIOFileSystem doesn't seem to support opening raw file descriptors.
//
// [1]: https://developer.apple.com/documentation/foundation/filehandle/bytes
while true {
do {
let data = try handle.read(upToCount: 64 * 1024)
if let data = data {
continuation.yield(data)
} else {
continuation.finish()
break
}
} catch (let error) {
continuation.finish(throwing: error)
break
}
}
} else {
handle.readabilityHandler = { handle in
let data = handle.availableData
continuation.yield(data)
if data.isEmpty {
continuation.finish()
if data.isEmpty {
continuation.finish()
} else {
continuation.yield(data)
}
}
}
}
group.addTask {
for await stdinData in stdinStream {
for try await stdinData in stdinStream {
try await execCall.requestStream.send(.with {
$0.type = .standardInput(.with {
$0.data = stdinData
})
})
}
// Signal EOF as we're done reading standard input
try await execCall.requestStream.send(.with {
$0.type = .standardInput(.with {
$0.data = Data()
})
})
}
}
@@ -178,3 +211,13 @@ struct Exec: AsyncParsableCommand {
}
}
}
private func isRegularFile(_ fileDescriptor: Int32) -> Bool {
var stat = stat()
if fstat(fileDescriptor, &stat) != 0 {
return false
}
return (stat.st_mode & S_IFMT) == S_IFREG
}
+2 -10
View File
@@ -116,10 +116,6 @@ struct Run: AsyncParsableCommand {
#endif
var vncExperimental: Bool = false
// Note that the valueName here should really be "path[:options]" instead of just "path",
// see ArgumentParser issue[1] for more details.
//
// [1]: https://github.com/apple/swift-argument-parser/issues/761
@Option(help: ArgumentHelp("""
Additional disk attachments with an optional read-only and synchronization options in the form of path[:options] (e.g. --disk="disk.bin", --disk="ubuntu.iso:ro", --disk="/dev/disk0", --disk "ghcr.io/cirruslabs/xcode:16.0:ro" or --disk="nbd://localhost:10809/myDisk:sync=none")
""", discussion: """
@@ -143,7 +139,7 @@ struct Run: AsyncParsableCommand {
To work around this pass TART_HOME explicitly:
sudo TART_HOME="$HOME/.tart" tart run sequoia --disk=/dev/disk0
""", valueName: "path"), completion: .file())
""", valueName: "path[:options]"), completion: .file())
var disk: [String] = []
#if arch(arm64)
@@ -162,10 +158,6 @@ struct Run: AsyncParsableCommand {
#endif
var rosettaTag: String?
// Note that the valueName here should really be "[name:]path[:options]" instead of just "path",
// see ArgumentParser issue[1] for more details.
//
// [1]: https://github.com/apple/swift-argument-parser/issues/761
@Option(help: ArgumentHelp("Additional directory shares with an optional read-only and mount tag options in the form of [name:]path[:options] (e.g. --dir=\"~/src/build\" or --dir=\"~/src/sources:ro\")", discussion: """
Requires host to be macOS 13.0 (Ventura) or newer. macOS guests must be running macOS 13.0 (Ventura) or newer too.
@@ -178,7 +170,7 @@ struct Run: AsyncParsableCommand {
Mount tag can be overridden by appending tag property to the directory share (e.g. --dir=\"~/src/build:tag=build\" or --dir=\"~/src/build:ro,tag=build\"). Then it can be mounted via "mount_virtiofs build ~/build" inside guest macOS and "mount -t virtiofs build ~/build" inside guest Linux.
In case of passing multiple directories per mount tag it is required to prefix them with names e.g. --dir=\"build:~/src/build\" --dir=\"sources:~/src/sources:ro\". These names will be used as directory names under the mounting point inside guests. For the example above it will be "/Volumes/My Shared Files/build" and "/Volumes/My Shared Files/sources" respectively.
""", valueName: "path"), completion: .directory)
""", valueName: "[name:]path[:options]"), completion: .directory)
var dir: [String] = []
@Flag(help: ArgumentHelp("Enable nested virtualization if possible"))
@@ -5,7 +5,7 @@ fileprivate func normalizeName(_ name: String) -> String {
return name.replacingOccurrences(of: ":", with: "\\:")
}
func completeMachines(_ arguments: [String]) -> [String] {
func completeMachines(_ arguments: [String], _ argumentIdx: Int, _ argumentPrefix: String) -> [String] {
let localVMs = (try? VMStorageLocal().list().map { name, _ in
normalizeName(name)
}) ?? []
@@ -15,12 +15,12 @@ func completeMachines(_ arguments: [String]) -> [String] {
return (localVMs + ociVMs)
}
func completeLocalMachines(_ arguments: [String]) -> [String] {
func completeLocalMachines(_ arguments: [String], _ argumentIdx: Int, _ argumentPrefix: String) -> [String] {
let localVMs = (try? VMStorageLocal().list()) ?? []
return localVMs.map { name, _ in normalizeName(name) }
}
func completeRunningMachines(_ arguments: [String]) -> [String] {
func completeRunningMachines(_ arguments: [String], _ argumentIdx: Int, _ argumentPrefix: String) -> [String] {
let localVMs = (try? VMStorageLocal().list()) ?? []
return localVMs
.filter { _, vmDir in (try? vmDir.state() == .Running) ?? false}
+6 -2
View File
@@ -27,12 +27,12 @@ class VMStorageOCI: PrunableStorage {
return digest
}
func open(_ name: RemoteName) throws -> VMDirectory {
func open(_ name: RemoteName, _ accessDate: Date = Date()) throws -> VMDirectory {
let vmDir = VMDirectory(baseURL: vmURL(name))
try vmDir.validate(userFriendlyName: name.description)
try vmDir.baseURL.updateAccessDate()
try vmDir.baseURL.updateAccessDate(accessDate)
return vmDir
}
@@ -180,6 +180,10 @@ class VMStorageOCI: PrunableStorage {
let transaction = SentrySDK.startTransaction(name: name.description, operation: "pull", bindToScope: true)
let tmpVMDir = try VMDirectory.temporaryDeterministic(key: name.description)
// Open an existing VM directory corresponding to this name, if any,
// marking it as outdated to speed up the garbage collection process
_ = try? open(name, Date(timeIntervalSince1970: 0))
// Lock the temporary VM directory to prevent it's garbage collection
let tmpVMDirLock = try FileLock(lockURL: tmpVMDir.baseURL)
try tmpVMDirLock.lock()
+1
View File
@@ -46,6 +46,7 @@ plugins:
match_path: blog/posts/.*
date_from_meta:
as_creation: date
abstract_chars_count: -1
- social:
cards_layout_dir: docs/layouts
cards_layout: custom