mirror of
https://github.com/xtool-org/xtool.git
synced 2026-10-05 17:35:23 +02:00
## Problem
`xtool setup` fails on Linux Mint (and other distros) with:
```
Error: Error Domain=NSCocoaErrorDomain Code=512 "(null)" UserInfo={
NSUserStringVariant=["Copy"],
NSSourceFilePathErrorKey=/tmp/sh.xtool/tmp-DarwinSDKBuild-.../darwin.artifactbundle/...,
NSDestinationFilePath=/home/<user>/.swiftpm/swift-sdks/darwin.artifactbundle/...,
NSUnderlyingError=Error Domain=NSPOSIXErrorDomain Code=22 "Invalid argument"
}
```
## Root Cause
On Linux Mint (and Ubuntu-based distros), `/tmp` is mounted as **tmpfs**
while the home directory is on **ext4**. Foundation's
`FileManager.copyItem` uses `sendfile(2)` internally, which returns
`EINVAL (22)` when copying between different filesystem types.
`TemporaryDirectoryRoot` defaults to
`FileManager.default.temporaryDirectory` → `/tmp` on Linux. When `swift
sdk install` copies the built SDK from `/tmp/sh.xtool/...` to
`~/.swiftpm/swift-sdks/`, it crosses filesystem boundaries and fails.
## Fix
On Linux, default the temp directory root to `$XDG_CACHE_HOME/xtool`
(falling back to `~/.cache/xtool`) instead of `/tmp`. This keeps the
build directory and install destination on the same filesystem, so the
copy always succeeds.
- `$XTL_TMPDIR` and `$TMPDIR` overrides continue to work as before
- macOS behaviour is unchanged
- Follows XDG Base Directory Specification
## Changes
One file, `Sources/XUtils/TemporaryDirectory.swift` — 11 lines added
inside a `#if os(Linux)` block.
Fixes #181
107 lines
3.9 KiB
Swift
107 lines
3.9 KiB
Swift
import Foundation
|
|
|
|
package struct TemporaryDirectory: ~Copyable {
|
|
private static let debugTmp = ProcessInfo.processInfo.environment["XTL_DEBUG_TMP"] != nil
|
|
|
|
private var shouldDelete: Bool
|
|
package let url: URL
|
|
|
|
/// Prepares a fresh tmpdir root.
|
|
///
|
|
/// Optional, but try calling this at launch to clean up old resources.
|
|
package static func prepare() {
|
|
_ = TemporaryDirectoryRoot.shared
|
|
}
|
|
|
|
/// Creates a temporary directory where `lastPathComponent` is exactly `name`.
|
|
///
|
|
/// The directory is deleted on deinit or (if the object never deinits) on next launch.
|
|
/// To save the contents, move them elsewhere with ``persist(at:)``.
|
|
package init(name: String) throws {
|
|
do {
|
|
let basename = name.replacingOccurrences(of: ".", with: "_")
|
|
self.url = try TemporaryDirectoryRoot.shared.url
|
|
// ensures uniqueness
|
|
.appendingPathComponent("tmp-\(basename)-\(UUID().uuidString)")
|
|
.appendingPathComponent(name, isDirectory: true)
|
|
self.shouldDelete = true
|
|
} catch {
|
|
// non-copyable types can't be partially initialized so we need a stub value
|
|
self.url = URL(fileURLWithPath: "")
|
|
self.shouldDelete = false
|
|
throw error
|
|
}
|
|
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
|
|
|
|
if Self.debugTmp {
|
|
stderrPrint("Created TemporaryDirectory: \(url.path)")
|
|
}
|
|
}
|
|
|
|
private func _delete() {
|
|
guard !Self.debugTmp else { return }
|
|
try? FileManager.default.removeItem(at: url)
|
|
}
|
|
|
|
package consuming func persist(at location: URL) throws {
|
|
try FileManager.default.moveItem(at: url, to: location)
|
|
// we do this after moving, so that if the move fails we clean up
|
|
shouldDelete = false
|
|
}
|
|
|
|
deinit {
|
|
if shouldDelete { _delete() }
|
|
}
|
|
}
|
|
|
|
private struct TemporaryDirectoryRoot {
|
|
static let shared = TemporaryDirectoryRoot()
|
|
|
|
private let _url: Result<URL, Errors>
|
|
var url: URL {
|
|
get throws(Errors) {
|
|
try _url.get()
|
|
}
|
|
}
|
|
|
|
private init() {
|
|
let url: URL
|
|
let env = ProcessInfo.processInfo.environment
|
|
if let tmpdir = env["XTL_TMPDIR"] ?? env["TMPDIR"] {
|
|
url = URL(fileURLWithPath: tmpdir).appendingPathComponent("sh.xtool")
|
|
} else {
|
|
#if os(Linux)
|
|
// On Linux, /tmp is commonly a tmpfs mount while ~/.swiftpm lives on ext4.
|
|
// Foundation's FileManager.copyItem uses sendfile(2) internally, which returns
|
|
// EINVAL when copying between different filesystem types (e.g. tmpfs → ext4).
|
|
// This causes `swift sdk install` to fail on distros like Linux Mint (#181).
|
|
// Using a cache dir on the home filesystem avoids the cross-fs copy entirely.
|
|
let xdgCache = env["XDG_CACHE_HOME"].map { URL(fileURLWithPath: $0) }
|
|
?? FileManager.default.homeDirectoryForCurrentUser.appendingPathComponent(".cache")
|
|
url = xdgCache.appendingPathComponent("xtool")
|
|
#else
|
|
url = FileManager.default.temporaryDirectory.appendingPathComponent("sh.xtool")
|
|
#endif
|
|
}
|
|
try? FileManager.default.removeItem(at: url)
|
|
do {
|
|
try FileManager.default.createDirectory(at: url, withIntermediateDirectories: true)
|
|
} catch {
|
|
self._url = .failure(Errors.tmpdirCreationFailed(url, error))
|
|
return
|
|
}
|
|
self._url = .success(url)
|
|
}
|
|
|
|
enum Errors: Error, CustomStringConvertible {
|
|
case tmpdirCreationFailed(URL, Error)
|
|
|
|
var description: String {
|
|
switch self {
|
|
case let .tmpdirCreationFailed(url, error):
|
|
"Could not create temporary directory at '\(url.path)': \(error)"
|
|
}
|
|
}
|
|
}
|
|
}
|