Files
xtool-mirror/Sources/XUtils/TemporaryDirectory.swift
Swapnil Nakate c65d45ba60 fix(linux): use home cache dir to avoid tmpfs→ext4 EINVAL on SDK install (#211)
## 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
2026-04-18 17:25:10 +05:30

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)"
}
}
}
}