Actions: Add Linux build step (#654)

Related to https://github.com/apple/containerization/issues/606

To make sure the project (and many linux specific libraries) builds on
linux lets add a linux step to build for both musl and glibc. Eventually
this will run unit tests as well. This change needs a bit more libc
agnostic-ifying work to make sure things work for both libcs.
This commit is contained in:
Danny Canter authored and GitHub committed 2026-04-07 15:13:12 -07:00
1 parent 1a91899b63
commit 997d5a4b08
15 files changed
+210 -45

No files matched your search

+40
View File
@@ -0,0 +1,40 @@
name: Linux build
permissions:
contents: read
on:
pull_request:
types: [opened, reopened, synchronize]
push:
branches:
- main
- release/*
jobs:
build:
name: Linux compile check
timeout-minutes: 30
runs-on: ubuntu-24.04
container: swift:6.3-noble
steps:
- name: Checkout repository
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6
with:
fetch-depth: 0
- name: Install system dependencies
run: apt-get update && apt-get install -y curl make libarchive-dev libbz2-dev liblzma-dev libssl-dev
- name: Build containerization
run: make containerization
- name: Build vminitd (glibc)
run: make -C vminitd SWIFT_CONFIGURATION="--disable-automatic-resolution -Xswiftc -warnings-as-errors"
- name: Install Static Linux SDK
run: make -C vminitd linux-sdk
- name: Build vminitd (musl)
run: make -C vminitd
+31
View File
@@ -0,0 +1,31 @@
/*
* 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.
*/
// The below fall into two main categories:
// 1. Aren't exposed by Swifts glibc modulemap.
// 2. Don't have syscall wrappers/definitions in glibc/musl.
#ifndef __LINUX_SHIM_H
#define __LINUX_SHIM_H
#if defined(__linux__)
#include <sys/epoll.h>
#include <sys/vfs.h>
#endif /* __linux__ */
#endif /* __LINUX_SHIM_H */
+28 -9
View File
@@ -14,24 +14,43 @@
// limitations under the License.
//===----------------------------------------------------------------------===//
#if os(Linux)
#if canImport(Musl)
import Musl
#elseif canImport(Glibc)
import Glibc
#endif
import CShim
import Foundation
import Synchronization
// On glibc, epoll constants are EPOLL_EVENTS enum values. On musl they're
// plain UInt32. These helpers normalize them to UInt32/Int32.
private func epollMask(_ value: UInt32) -> UInt32 { value }
private func epollMask(_ value: Int32) -> UInt32 { UInt32(bitPattern: value) }
#if canImport(Glibc)
private func epollMask(_ value: EPOLL_EVENTS) -> UInt32 { value.rawValue }
private func epollFlag(_ value: EPOLL_EVENTS) -> Int32 { Int32(bitPattern: value.rawValue) }
#endif
/// Register file descriptors to receive events via Linux's
/// epoll syscall surface.
public final class Epoll: Sendable {
public typealias Mask = Int32
public typealias Handler = (@Sendable (Mask) -> Void)
public static let maskIn: Int32 = Int32(bitPattern: epollMask(EPOLLIN))
public static let maskOut: Int32 = Int32(bitPattern: epollMask(EPOLLOUT))
public static let defaultMask: Int32 = maskIn | maskOut
private let epollFD: Int32
private let handlers = SafeMap<Int32, Handler>()
private let pipe = Pipe() // to wake up a waiting epoll_wait
public init() throws {
let efd = epoll_create1(EPOLL_CLOEXEC)
let efd = epoll_create1(Int32(EPOLL_CLOEXEC))
guard efd > 0 else {
throw POSIXError.fromErrno()
}
@@ -41,14 +60,14 @@ public final class Epoll: Sendable {
public func add(
_ fd: Int32,
mask: Int32 = EPOLLIN | EPOLLOUT, // HUP is always added
mask: Int32 = Epoll.defaultMask,
handler: @escaping Handler
) throws {
guard fcntl(fd, F_SETFL, O_NONBLOCK) == 0 else {
throw POSIXError.fromErrno()
}
let events = EPOLLET | UInt32(bitPattern: mask)
let events = epollMask(EPOLLET) | UInt32(bitPattern: mask)
var event = epoll_event()
event.events = events
@@ -115,7 +134,7 @@ public final class Epoll: Sendable {
public func delete(_ fd: Int32) throws {
var event = epoll_event()
let result = withUnsafeMutablePointer(to: &event) { ptr in
epoll_ctl(self.epollFD, EPOLL_CTL_DEL, fd, ptr)
epoll_ctl(self.epollFD, EPOLL_CTL_DEL, fd, ptr) as Int32
}
if result != 0 {
if !acceptableDeletionErrno() {
@@ -162,20 +181,20 @@ public final class Epoll: Sendable {
extension Epoll.Mask {
public var isHangup: Bool {
(self & (EPOLLHUP | EPOLLERR)) != 0
(self & Int32(bitPattern: epollMask(EPOLLHUP) | epollMask(EPOLLERR))) != 0
}
public var isRhangup: Bool {
(self & EPOLLRDHUP) != 0
(self & Int32(bitPattern: epollMask(EPOLLRDHUP))) != 0
}
public var readyToRead: Bool {
(self & EPOLLIN) != 0
(self & Int32(bitPattern: epollMask(EPOLLIN))) != 0
}
public var readyToWrite: Bool {
(self & EPOLLOUT) != 0
(self & Int32(bitPattern: epollMask(EPOLLOUT))) != 0
}
}
#endif // canImport(Musl)
#endif // os(Linux)
+4 -3
View File
@@ -31,6 +31,7 @@ SWIFT_CONFIGURATION := --swift-sdk $(MUSL_ARCH)-swift-linux-musl $(SWIFT_WARNING
SWIFT_VERSION := 6.3
SWIFT_SDK_URL := https://download.swift.org/swift-6.3-release/static-sdk/swift-6.3-RELEASE/swift-6.3-RELEASE_static-linux-0.1.0.artifactbundle.tar.gz
SWIFT_SDK_CHECKSUM := d2078b69bdeb5c31202c10e9d8a11d6f66f82938b51a4b75f032ccb35c4c286c
SWIFT_SDK_PATH := /tmp/$(notdir $(SWIFT_SDK_URL))
SYSTEM_TYPE := $(shell uname -s)
@@ -64,7 +65,7 @@ all:
@install "$(BUILD_BIN_DIR)/vmexec" ./bin/
.PHONY: cross-prep
cross-prep: linux-sdk
cross-prep: swift linux-sdk
.PHONY: swiftly
swiftly:
@@ -84,10 +85,10 @@ swift: swiftly
@${SWIFTLY_BIN_DIR}/swiftly install $(SWIFT_VERSION)
.PHONY: linux-sdk
linux-sdk: swift
linux-sdk:
@echo Installing Static Linux SDK...
@curl -L -o $(SWIFT_SDK_PATH) $(SWIFT_SDK_URL)
-@$(SWIFT) sdk install $(SWIFT_SDK_PATH)
-@$(SWIFT) sdk install $(SWIFT_SDK_PATH) --checksum $(SWIFT_SDK_CHECKSUM)
@rm $(SWIFT_SDK_PATH)
.PHONY: clean
+1
View File
@@ -56,6 +56,7 @@ let package = Package(
.product(name: "ContainerizationOCI", package: "containerization"),
.product(name: "ContainerizationOS", package: "containerization"),
.product(name: "SystemPackage", package: "swift-system"),
"LCShim",
]
),
.executableTarget(
@@ -26,6 +26,7 @@ import Musl
import Glibc
#endif
import LCShim
import ContainerizationOCI
import ContainerizationOS
import Foundation
+65 -3
View File
@@ -14,25 +14,87 @@
* limitations under the License.
*/
// The below fall into two main categories:
// 1. Aren't exposed by Swifts glibc modulemap.
// 2. Don't have syscall wrappers/definitions in glibc/musl.
#ifndef __SYSCALL_H
#define __SYSCALL_H
#include <sys/types.h>
#include <sys/vfs.h>
// CLONE_* flags
#ifndef CLONE_NEWNS
#define CLONE_NEWNS 0x00020000
#endif
#ifndef CLONE_NEWCGROUP
#define CLONE_NEWCGROUP 0x02000000
#endif
#ifndef CLONE_NEWUTS
#define CLONE_NEWUTS 0x04000000
#endif
#ifndef CLONE_NEWIPC
#define CLONE_NEWIPC 0x08000000
#endif
#ifndef CLONE_NEWUSER
#define CLONE_NEWUSER 0x10000000
#endif
#ifndef CLONE_NEWPID
#define CLONE_NEWPID 0x20000000
#endif
extern int setns(int fd, int nstype);
extern int unshare(int flags);
extern int dup3(int oldfd, int newfd, int flags);
extern int execvpe(const char *file, char *const argv[], char *const envp[]);
extern int unlockpt(int fd);
extern char *ptsname(int fd);
// splice(2) and flags.
extern ssize_t splice(int fd_in, off_t *off_in, int fd_out, off_t *off_out,
size_t len, unsigned int flags);
#ifndef SPLICE_F_MOVE
#define SPLICE_F_MOVE 1
#endif
#ifndef SPLICE_F_NONBLOCK
#define SPLICE_F_NONBLOCK 2
#endif
// RLIMIT constants as plain integers. On glibc these are __rlimit_resource
// enum values which can't be used as Int32 in Swift.
#define CZ_RLIMIT_CPU 0
#define CZ_RLIMIT_FSIZE 1
#define CZ_RLIMIT_DATA 2
#define CZ_RLIMIT_STACK 3
#define CZ_RLIMIT_CORE 4
#define CZ_RLIMIT_RSS 5
#define CZ_RLIMIT_NPROC 6
#define CZ_RLIMIT_NOFILE 7
#define CZ_RLIMIT_MEMLOCK 8
#define CZ_RLIMIT_AS 9
#define CZ_RLIMIT_LOCKS 10
#define CZ_RLIMIT_SIGPENDING 11
#define CZ_RLIMIT_MSGQUEUE 12
#define CZ_RLIMIT_NICE 13
#define CZ_RLIMIT_RTPRIO 14
#define CZ_RLIMIT_RTTIME 15
// setrlimit wrapper that accepts plain int for the resource parameter,
// avoiding glibc's __rlimit_resource enum type mismatch in Swift.
int CZ_setrlimit(int resource, unsigned long long soft, unsigned long long hard);
int CZ_pivot_root(const char *new_root, const char *put_old);
int CZ_set_sub_reaper();
#ifndef SYS_pidfd_open
#define SYS_pidfd_open 434
#endif
int CZ_pidfd_open(pid_t pid, unsigned int flags);
#ifndef SYS_pidfd_getfd
#define SYS_pidfd_getfd 438
#endif
int CZ_pidfd_getfd(int pidfd, int targetfd, unsigned int flags);
int CZ_prctl_set_no_new_privs();
+9
View File
@@ -15,6 +15,7 @@
*/
#include <sys/prctl.h>
#include <sys/resource.h>
#include <sys/syscall.h>
#include <unistd.h>
@@ -39,3 +40,11 @@ int CZ_pidfd_getfd(int pidfd, int targetfd, unsigned int flags) {
int CZ_prctl_set_no_new_privs() {
return prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
}
int CZ_setrlimit(int resource, unsigned long long soft,
unsigned long long hard) {
struct rlimit limit;
limit.rlim_cur = (rlim_t)soft;
limit.rlim_max = (rlim_t)hard;
return setrlimit(resource, &limit);
}
+1
View File
@@ -15,6 +15,7 @@
//===----------------------------------------------------------------------===//
import FoundationEssentials
import LCShim
#if canImport(Musl)
import Musl
+1 -1
View File
@@ -81,7 +81,7 @@ struct RunCommand: ParsableCommand {
guard statfs("/", &s) == 0 else {
throw App.Errno(stage: "statfs(/)")
}
flags |= s.f_flags
flags |= UInt(s.f_flags)
guard mount("", "/", "", flags, "") == 0 else {
throw App.Errno(stage: "mount rootfs ro")
+17 -18
View File
@@ -158,46 +158,45 @@ extension App {
static func setRLimits(rlimits: [ContainerizationOCI.POSIXRlimit]) throws {
for rl in rlimits {
var limit = rlimit(rlim_cur: rl.soft, rlim_max: rl.hard)
let resource: Int32
switch rl.type {
case "RLIMIT_AS":
resource = RLIMIT_AS
resource = CZ_RLIMIT_AS
case "RLIMIT_CORE":
resource = RLIMIT_CORE
resource = CZ_RLIMIT_CORE
case "RLIMIT_CPU":
resource = RLIMIT_CPU
resource = CZ_RLIMIT_CPU
case "RLIMIT_DATA":
resource = RLIMIT_DATA
resource = CZ_RLIMIT_DATA
case "RLIMIT_FSIZE":
resource = RLIMIT_FSIZE
resource = CZ_RLIMIT_FSIZE
case "RLIMIT_LOCKS":
resource = RLIMIT_LOCKS
resource = CZ_RLIMIT_LOCKS
case "RLIMIT_MEMLOCK":
resource = RLIMIT_MEMLOCK
resource = CZ_RLIMIT_MEMLOCK
case "RLIMIT_MSGQUEUE":
resource = RLIMIT_MSGQUEUE
resource = CZ_RLIMIT_MSGQUEUE
case "RLIMIT_NICE":
resource = RLIMIT_NICE
resource = CZ_RLIMIT_NICE
case "RLIMIT_NOFILE":
resource = RLIMIT_NOFILE
resource = CZ_RLIMIT_NOFILE
case "RLIMIT_NPROC":
resource = RLIMIT_NPROC
resource = CZ_RLIMIT_NPROC
case "RLIMIT_RSS":
resource = RLIMIT_RSS
resource = CZ_RLIMIT_RSS
case "RLIMIT_RTPRIO":
resource = RLIMIT_RTPRIO
resource = CZ_RLIMIT_RTPRIO
case "RLIMIT_RTTIME":
resource = RLIMIT_RTTIME
resource = CZ_RLIMIT_RTTIME
case "RLIMIT_SIGPENDING":
resource = RLIMIT_SIGPENDING
resource = CZ_RLIMIT_SIGPENDING
case "RLIMIT_STACK":
resource = RLIMIT_STACK
resource = CZ_RLIMIT_STACK
default:
errno = EINVAL
throw App.Errno(stage: "rlimit key unknown")
}
guard setrlimit(resource, &limit) == 0 else {
guard CZ_setrlimit(resource, rl.soft, rl.hard) == 0 else {
throw App.Errno(stage: "setrlimit()")
}
}
+2 -3
View File
@@ -192,12 +192,11 @@ struct AgentCommand: AsyncParsableCommand {
private static func adjustLimits(_ log: Logger) throws {
let nrOpen = try String(contentsOfFile: "/proc/sys/fs/nr_open", encoding: .utf8)
.trimmingCharacters(in: .whitespacesAndNewlines)
guard let max = rlim_t(nrOpen) else {
guard let max = UInt64(nrOpen) else {
throw POSIXError(.EINVAL)
}
log.debug("setting RLIMIT_NOFILE to \(max)")
var limits = rlimit(rlim_cur: max, rlim_max: max)
guard setrlimit(RLIMIT_NOFILE, &limits) == 0 else {
guard CZ_setrlimit(CZ_RLIMIT_NOFILE, max, max) == 0 else {
throw POSIXError(.init(rawValue: errno)!)
}
}
+1 -1
View File
@@ -118,7 +118,7 @@ final class IOPair: Sendable {
let readFrom = OSFile(fd: readFromFd)
let writeTo = OSFile(fd: writeToFd)
try ProcessSupervisor.default.poller.add(readFromFd, mask: EPOLLIN) { mask in
try ProcessSupervisor.default.poller.add(readFromFd, mask: Epoll.maskIn) { mask in
self.io.withLock { io in
if io.closed {
return
+3 -2
View File
@@ -15,6 +15,7 @@
//===----------------------------------------------------------------------===//
import Foundation
import LCShim
extension OSFile {
struct SpliceFile: Sendable {
@@ -61,7 +62,7 @@ extension OSFile {
while true {
while (from.offset - to.offset) < count {
let toRead = count - (from.offset - to.offset)
let bytesRead = Foundation.splice(from.fileDescriptor, nil, to.writer, nil, toRead, UInt32(bitPattern: SPLICE_F_MOVE | SPLICE_F_NONBLOCK))
let bytesRead = LCShim.splice(from.fileDescriptor, nil, to.writer, nil, toRead, UInt32(bitPattern: LCShim.SPLICE_F_MOVE | LCShim.SPLICE_F_NONBLOCK))
if bytesRead == -1 {
if errno != EAGAIN && errno != EIO {
throw POSIXError(.init(rawValue: errno)!)
@@ -81,7 +82,7 @@ extension OSFile {
}
while to.offset < from.offset {
let toWrite = from.offset - to.offset
let bytesWrote = Foundation.splice(to.reader, nil, to.fileDescriptor, nil, toWrite, UInt32(bitPattern: SPLICE_F_MOVE | SPLICE_F_NONBLOCK))
let bytesWrote = LCShim.splice(to.reader, nil, to.fileDescriptor, nil, toWrite, UInt32(bitPattern: LCShim.SPLICE_F_MOVE | LCShim.SPLICE_F_NONBLOCK))
if bytesWrote == -1 {
if errno != EAGAIN && errno != EIO {
throw POSIXError(.init(rawValue: errno)!)
+6 -5
View File
@@ -17,6 +17,7 @@
import ContainerizationIO
import ContainerizationOS
import Foundation
import LCShim
import Logging
actor VsockProxy {
@@ -243,7 +244,7 @@ extension VsockProxy {
c.resume()
}
try! ProcessSupervisor.default.poller.add(clientFile.fileDescriptor, mask: EPOLLIN | EPOLLOUT) { mask in
try! ProcessSupervisor.default.poller.add(clientFile.fileDescriptor) { mask in
if mask.readyToRead && !eofFromClient {
let (fromEof, toEof) = Self.transferData(
fromFile: &clientFile,
@@ -274,7 +275,7 @@ extension VsockProxy {
// we should see no more EPOLLIN events on the client fd
// and no more EPOLLOUT events on the server fd
eofFromClient = true
if shutdown(serverFile.fileDescriptor, SHUT_WR) != 0 {
if shutdown(serverFile.fileDescriptor, Int32(SHUT_WR)) != 0 {
self.log?.warning(
"failed to shut down client reads",
metadata: [
@@ -295,7 +296,7 @@ extension VsockProxy {
}
}
try! ProcessSupervisor.default.poller.add(serverFile.fileDescriptor, mask: EPOLLIN | EPOLLOUT) { mask in
try! ProcessSupervisor.default.poller.add(serverFile.fileDescriptor) { mask in
if mask.readyToRead && !eofFromServer {
let (fromEof, toEof) = Self.transferData(
fromFile: &serverFile,
@@ -326,7 +327,7 @@ extension VsockProxy {
// we should see no more EPOLLIN events on the server fd
// and no more EPOLLOUT events on the client fd
eofFromServer = true
if shutdown(clientFile.fileDescriptor, SHUT_WR) != 0 {
if shutdown(clientFile.fileDescriptor, Int32(SHUT_WR)) != 0 {
self.log?.warning(
"failed to shut down server reads",
metadata: [
@@ -375,7 +376,7 @@ extension VsockProxy {
// half close, shut down client to server transfer
// we should see no more EPOLLIN events on the client fd
// and no more EPOLLOUT events on the server fd
if shutdown(toFile.fileDescriptor, SHUT_WR) != 0 {
if shutdown(toFile.fileDescriptor, Int32(SHUT_WR)) != 0 {
log?.warning(
"failed to shut down reads",
metadata: [