[Pitch] Add an Embedded File System to SwiftPM

Hi all,

I would like to discuss adding a generated, read-only EmbeddedFileSystem to
SwiftPM. It would let a target access resources declared with SwiftPM's
existing Resource.embedInCode(_:) rule as a path-preserving file system.

The intended use cases are self-contained command-line and server executables,
WASI programs, test fixtures, templates, and static web content: cases where
raw resources should travel with the linked program without requiring a
Foundation bundle or a separately deployed resource directory.

This pitch does not propose a new resource declaration rule. The existing
.embedInCode rule would populate the embedded file system.

Background

SE-0271: Package Manager
Resources

introduced target-scoped resources and Bundle.module. SwiftPM later added
Resource.embedInCode(_:),
available since PackageDescription 5.9.

Today, embedding a resource generates a property similar to:

struct PackageResources {
    static let identifier_txt: [UInt8] = [72, 101, 108, 108, 111, /* ... */]
}

This is useful for small, individual files, but it has two important
limitations.

First, the generated API is a collection of properties derived from file
basenames. SwiftPM does not generate an EmbeddedFileSystem with a directory
hierarchy that can be traversed or queried by path. A directory of templates or
static web files therefore cannot be consumed as a file system.

Second, the native SwiftPM implementation currently renders every byte as a
Swift integer literal. The implementation itself contains a FIXME noting that
this does not work well for large files. The open issue
#75288 reports a roughly
200-second debug build for a single 165 KB resource, with larger resources
eventually exceeding the type checker's limits.

As a result, users currently choose between:

  • .process or .copy plus Bundle.module, which uses Foundation and deploys
    resources separately from a standalone executable; or
  • .embedInCode, which is Foundation-free and linked into the program, but
    exposes individual byte arrays and does not scale to resource trees or large
    files.

Proposed embedded file system

The manifest declaration would remain unchanged:

let package = Package(
    name: "ExampleServer",
    targets: [
        .executableTarget(
            name: "ExampleServer",
            resources: [
                .embedInCode("Web")
            ]
        )
    ]
)

Given this target layout:

Sources/ExampleServer/
β”œβ”€β”€ main.swift
└── Web/
    β”œβ”€β”€ index.html
    β”œβ”€β”€ css/site.css
    └── images/logo.svg

SwiftPM would preserve the target-relative logical paths and generate a
module-scoped accessor provisionally spelled:

let resources = PackageResources.fileSystem

guard let index = resources.file(at: "Web/index.html") else {
    // Missing resource
}

for entry in resources.entries(in: "Web/images") {
    print(entry.name)
}

The exact API needs discussion, but the intended minimum functionality is:

  • look up an entry by its logical path;
  • distinguish files and directories;
  • access a file's bytes without Foundation;
  • list the immediate entries of a directory; and
  • use the same path syntax and behavior on every supported platform.

An illustrative APIβ€”not yet a detailed proposalβ€”could look like this:

struct PackageResources {
    static let fileSystem: EmbeddedFileSystem

    struct EmbeddedFileSystem: Sendable {
        func entry(at path: String) -> Entry?
        func file(at path: String) -> File?
        func entries(in directory: String = "") -> [Entry]
    }

    struct Entry: Sendable {
        enum Kind: Sendable {
            case file
            case directory
        }

        let path: String
        let name: String
        let kind: Kind
    }

    struct File: RandomAccessCollection, Sendable {
        typealias Element = UInt8
        // Collection requirements omitted from this sketch.
    }
}

Nesting the generated types under PackageResources is only one possible way
to avoid adding a runtime module or new standard-library API. The spelling and
ownership of these types are important questions for the pitch.

Like Bundle.module, the generated API would initially be internal to the
target. A library author who wants clients to access its embedded resources can
deliberately expose an appropriate public API.

Logical path semantics

Embedded resource paths describe a virtual tree, not locations in the host
filesystem. I propose that they therefore have platform-independent semantics:

  • paths are relative and use / as the separator on every platform;
  • matching is case-sensitive;
  • path components are non-empty UTF-8 strings;
  • absolute paths and ., .., NUL, and empty components are rejected;
  • the empty string identifies the embedded root; and
  • directory listing has deterministic ordering.

In particular, this API should not use the host platform's native FilePath
syntax. An embedded path should not change meaning when the same package is
built on Windows rather than Linux or Darwin.

For .embedInCode("Web"), the example above uses Web/index.html, preserving
the target-relative path that was declared. Whether the declaration root itself
should be retained is one point on which I would particularly welcome feedback.

This model is broadly inspired by Go's
embed.FS, whose
embedded paths are slash-separated and independent of the host platform. This
pitch does not propose adopting Go's source-directive syntax.

Storage and build behavior

The public API should not expose an object-file layout.

One possible implementation is a raw byte blob plus a compact, sorted index
containing logical paths, entry kinds, offsets, and lengths. The bytes would be
placed directly in an object file or read-only object section rather than
written as Swift source. The generated Swift source would then contain only the
small accessor and metadata needed to locate that data.

Conceptually:

linked image
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ program code                β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ path β†’ kind/offset/length   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ raw resource bytes          β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

SwiftPM would be free to use different mechanisms for Mach-O, ELF, COFF, and
WASM, or a portable fallback where necessary. Section names, generated symbols,
alignment, and the number of blobs would remain implementation details.

Directly embedding bytes is not required for the filesystem semantics, but it
is important to making the existing .embedInCode rule practical for realistic
resource trees. It also avoids making Swift parser and type-checker performance
proportional to the textual decimal representation of every resource byte.

The representation improvement is separable from the proposed filesystem API:
it could be implemented independently as an optimization of today's generated
properties. The evolution proposal only needs to standardize user-visible
semantics, not a particular linker technique.

The byte-access API still needs a deliberate design. A copied [UInt8] is
simple and compatible with today's generated properties, while a collection or
scoped Span/RawSpan view could permit zero-copy access. The final proposal
must specify ownership, lifetime, bounds checking, and Sendable behavior
without exposing unsafe pointers as the primary interface.

Relationship to existing resource APIs

This is intended to complement, not replace, Bundle.module:

  • .process remains appropriate for localized or platform-processed resources;
  • .copy remains appropriate when resources should be deployed as files;
  • .embedInCode remains appropriate for immutable raw bytes that should be
    linked with the program.

The new EmbeddedFileSystem accessor would be generated when a target contains one or
more .embedInCode resources.

Existing generated PackageResources.<mangledName>: [UInt8] properties could
remain available for source compatibility. Their implementation could load
from the same embedded blob instead of containing a Swift array literal.
Resources with duplicate basenames in different directoriesβ€”which the current
property model cannot representβ€”would still be addressable by full logical
path through the filesystem accessor.

Any new generated API or changed path behavior would be gated by a new Swift
tools version.

Why SwiftPM rather than a package plugin?

A package plugin could generate a resource table, but each implementation would
need to reproduce SwiftPM's target resource discovery, dependency tracking,
linker integration, and platform-specific object generation. It would also add
a plugin and generator dependency to every adopting package.

SwiftPM already owns the .embedInCode rule and knows the complete set of
target resources. Providing the generated accessor there gives packages one
consistent behavior across command-line SwiftPM and build-system integrations.

Security and reproducibility

The build should reject logical paths that could escape the resource root and
should diagnose duplicate logical paths. The implementation must validate
offset and length metadata before exposing a byte view and avoid integer
overflow for large resources.

Generated output should be deterministic. Only logical names, file/directory
kind, offsets, lengths, and file contents need to be represented. Source
timestamps, permissions, extended attributes, and absolute build-machine paths
should not be embedded.

Symlink handling needs an explicit rule. My initial preference is to reject
symlinked embedded resources so that a resource declaration cannot
unintentionally capture content outside its package, but I would welcome input
on compatibility with existing SwiftPM resource behavior.

Alternatives considered

Continue generating individual [UInt8] properties

This is simple for callers embedding one small file, but it does not model a
directory and generates very large Swift expressions. Improving array-literal
type checking may reduce compilation time but would not provide path-preserving
lookup or avoid generating source proportional to the resource size.

Use only Bundle.module

Bundle.module is the right abstraction for many application resources, but it
requires Foundation and generally leaves a resource directory or bundle to be
deployed with the executable. It does not satisfy Foundation-free or
single-linked-artifact use cases.

Introduce a new resource rule

A spelling such as .embedFileSystem("Web") would make the new behavior
explicit, but it would overlap heavily with the existing meaning of
.embedInCode. My preference is to complete the existing rule rather than add
a second way to request that the same bytes be linked into the program.

Introduce a general filesystem protocol now

A shared read-only protocol could eventually make embedded resources,
Bundle, and on-disk directories interchangeable. However, such a protocol
needs an owning runtime module and could expand the proposal into Foundation or
standard-library evolution. This pitch keeps the first step focused on
SwiftPM-generated embedded resources while leaving that abstraction as a
future direction.

Questions for discussion

I would particularly appreciate feedback on these points:

  1. Is PackageResources.fileSystem a suitable generated entry point, and should
    its concrete supporting types be nested under PackageResources?
  2. Should .embedInCode("Web") expose Web/index.html, as proposed here, or
    mount the contents of Web directly at the embedded root?
  3. What should the primary byte-access API be: a collection value, a scoped
    span, or an owned [UInt8]?
  4. Are exact lookup, file/directory metadata, whole-file byte access, and
    immediate directory listing the right minimum filesystem surface?
  5. Should existing generated per-file properties remain indefinitely, or be
    considered for later deprecation after the filesystem accessor is available?
  6. Are there build-system or object-format constraints that make a blob plus
    sorted path index unsuitable on any SwiftPM-supported platform?
  7. Should symlinks always be rejected for embedded resources?

If the overall direction is sound, I plan to turn the result of this discussion
into a SwiftPM evolution proposal and a focused implementation prototype.

7 Likes

These days I would use a plugin to do this sort of thing. You can roll it out today, and sort out all the kinks there.

I feel like the embedded FS issue is orthogonal to the need for a #embed/include_bytes! equivalent. In that your proposal requires that language change, and that language change is useful immediately, without the rest of your proposal. I'd start there.

Previously on these forums somebody from the Swift team has expressed distaste for the idea of doing this, IIRC because it means that the compiler needs to load an input file which isn't mentioned on the command-line, and therefore potentially untracked as a dependency. So your solution would need to navigate those waters first, before taking on whatever SPM and swift-build integration is required.

We definitely need to revisit how resources are done. As @migueldeicaza suggested, plugins would be the preferred way to do that. We are looking at ways to extend the capabilities of plugins so it's a good opportunity for someone to study what resources would need from them.

3 Likes

Also, for what it's worth, MSVC solved this decades ago with their resource compiler that let you link resources into your executable. Something similar for Swift would be interesting.

1 Like

NeXTSTEP and classic Mac OS did the same thing. It isn’t an unknown technique.