Files
apple--containerization/Sources/Containerization/FileMount.swift
T
wehub-resource-sync 680845cb1c
Linux build / Determine Swift version (push) Waiting to run
Linux build / Linux compile check (push) Blocked by required conditions
Build containerization / Verify commit signatures (push) Has been skipped
Build containerization / containerization (push) Successful in 0s
chore: import upstream snapshot with attribution
2026-07-13 12:25:30 +08:00

203 lines
7.9 KiB
Swift

//===----------------------------------------------------------------------===//
// Copyright © 2026 Apple Inc. and the Containerization project authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//===----------------------------------------------------------------------===//
import ContainerizationError
import ContainerizationOCI
import Foundation
/// Manages single-file mounts by transforming them into virtiofs directory shares
/// plus bind mounts.
///
/// Since virtiofs only supports sharing directories, mounting a single file requires
/// sharing the file's parent directory via virtiofs and then bind mounting the specific
/// file from that share to the final destination in the container.
struct FileMountContext: Sendable {
/// Metadata for a single prepared file mount.
struct PreparedMount: Sendable {
/// Original file path on host
let hostFilePath: String
/// Where the user wants the file in the container
let containerDestination: String
/// Just the filename (after resolving symlinks)
let filename: String
/// The parent directory containing the file (after resolving symlinks)
let parentDirectory: URL
/// The virtiofs tag (hash of parent dir path). Used to find the AttachedFilesystem
let tag: String
/// Mount options from the original mount
let options: [String]
/// Where we mounted the share in the guest (set after mountHoldingDirectories)
var guestHoldingPath: String?
}
/// Prepared file mounts for this context
var preparedMounts: [PreparedMount]
/// The transformed mounts to pass to the VM (files replaced with directory shares)
private(set) var transformedMounts: [Mount]
private init() {
self.preparedMounts = []
self.transformedMounts = []
}
/// Returns true if there are any file mounts that need handling.
var hasFileMounts: Bool {
!preparedMounts.isEmpty
}
/// Returns the set of virtiofs tags for file mount holding directories.
/// These should be filtered out from OCI spec mounts since we mount them
/// separately under /run.
var holdingDirectoryTags: Set<String> {
Set(preparedMounts.map { $0.tag })
}
}
extension FileMountContext {
/// Prepare mounts for a container, detecting file mounts and transforming them.
///
/// This method stats each virtiofs mount source. If it's a regular file rather than
/// a directory, it shares the file's parent directory via virtiofs and records the
/// metadata needed to bind mount the specific file later.
///
/// - Parameter mounts: The original mounts from the container config
/// - Returns: A FileMountContext containing transformed mounts and tracking info
static func prepare(mounts: [Mount]) throws -> FileMountContext {
var context = FileMountContext()
var transformed: [Mount] = []
// Track parent directories we've already added a share for to avoid duplicates.
var sharedParentTags: Set<String> = []
for mount in mounts {
// Only virtiofs mounts can be files
guard case .virtiofs(let runtimeOpts) = mount.runtimeOptions else {
transformed.append(mount)
continue
}
// Stat the source to see if it's a file
let fm = FileManager.default
var isDirectory: ObjCBool = false
guard fm.fileExists(atPath: mount.source, isDirectory: &isDirectory) else {
// Doesn't exist. Let the normal flow handle the error
transformed.append(mount)
continue
}
if isDirectory.boolValue {
// It's a directory, pass through unchanged
transformed.append(mount)
continue
}
// It's a file, so prepare it.
let prepared = try context.prepareFileMount(mount: mount, runtimeOptions: runtimeOpts)
// Only add the directory share once per unique parent directory.
if !sharedParentTags.contains(prepared.tag) {
sharedParentTags.insert(prepared.tag)
// The destination here is unused. We mount the share ourselves
// to a location under /run in mountHoldingDirectories.
let directoryShare = Mount.share(
source: prepared.parentDirectory.path,
destination: "/.file-mount-holding",
options: mount.options.filter { $0 != "bind" },
runtimeOptions: runtimeOpts
)
transformed.append(directoryShare)
}
}
context.transformedMounts = transformed
return context
}
private mutating func prepareFileMount(
mount: Mount,
runtimeOptions: [String]
) throws -> PreparedMount {
let resolvedSource = URL(fileURLWithPath: mount.source).resolvingSymlinksInPath()
let filename = resolvedSource.lastPathComponent
let parentDirectory = resolvedSource.deletingLastPathComponent()
let tag = try hashFilePath(path: parentDirectory.path)
let prepared = PreparedMount(
hostFilePath: mount.source,
containerDestination: mount.destination,
filename: filename,
parentDirectory: parentDirectory,
tag: tag,
options: mount.options,
guestHoldingPath: nil
)
preparedMounts.append(prepared)
return prepared
}
}
extension FileMountContext {
/// Set up the holding directory paths for all file mounts.
/// Since virtiofs shares are now mounted once at /run/virtiofs, the holding
/// directories appear as subdirectories there automatically.
/// - Parameters:
/// - vmMounts: The AttachedFilesystem array from the VM for this container
/// - agent: The VM agent for RPCs (unused, kept for API compatibility)
mutating func mountHoldingDirectories(
vmMounts: [AttachedFilesystem],
agent: any VirtualMachineAgent
) async throws {
for i in preparedMounts.indices {
let prepared = preparedMounts[i]
// Verify the attached filesystem exists
guard
vmMounts.first(where: {
$0.type == "virtiofs" && $0.source == prepared.tag
}) != nil
else {
throw ContainerizationError(
.notFound,
message: "could not find attached filesystem for file mount \(prepared.hostFilePath)"
)
}
// With unified virtiofs, holding directories are subdirectories under /run/virtiofs
let guestPath = "/run/virtiofs/\(prepared.tag)"
preparedMounts[i].guestHoldingPath = guestPath
}
}
}
extension FileMountContext {
/// Get the bind mounts to append to the OCI spec.
func ociBindMounts() -> [ContainerizationOCI.Mount] {
preparedMounts.compactMap { prepared in
guard let guestPath = prepared.guestHoldingPath else {
return nil
}
return ContainerizationOCI.Mount(
type: "none",
source: "\(guestPath)/\(prepared.filename)",
destination: prepared.containerDestination,
options: ["bind"] + prepared.options
)
}
}
}