The .scppkg Package
Format
This document specifies .scppkg: a package bundling one
or more scpp modules for distribution – as raw .scpp
interface source, or as a .scppm interface (see The .scppm Module Interface
Format) paired with one .scppa archive per target
triple, or a mix – into one distributable archive.
.scppkg is referenced from ch11
§11.12 of the language specification.
A .scppa file, named from a .scppkg
manifest (§3), is not a format this document defines: it is exactly the
target platform’s own native static-library archive format (the
ar format for a Unix .a, or a Windows
.lib), bundling one .scppo object member per
file that contributed code to a module – its primary interface unit, an
implementation unit, or a partition (see ch11 §11.3
and §11.4)
– for one target triple. Because it is a native archive, a system linker
reads a .scppa file directly and unpacks it exactly as it
would any other static library; nothing about it is scpp-specific except
which .scppo members happen to be inside, and members need
not even be named .scppo internally for that to work (a
linker locates symbols via the archive’s own native index, not member
names).
A .scppo file, in turn, is also not a format this
document defines: it is exactly the target platform’s own native object
file format (ELF, COFF, Mach-O, etc.).
1. Envelope
A .scppkg file wraps a tar
archive in an envelope:
[Header] -- fixed size, 8 bytes, always stored raw
[payload_length] -- uint64, little-endian, 8 bytes
[Payload] -- payload_length bytes: a tar archive, raw or a single LZMA stream, per the header's flags
[Signature block] -- optional, trailing, present only if flags bit 4 is set (§4)
Header
A fixed 8-byte header, never compressed:
| Offset | Size | Field | Value |
|---|---|---|---|
| 0 | 5 | magic |
ASCII bytes SCPPK. |
| 5 | 1 | major_version |
uint8. This document defines major version 1. |
| 6 | 1 | patch_version |
uint8. This document defines patch version 0. |
| 7 | 1 | flags |
Bits 3-0, bit 4, bits 5-7 (below). |
Bits of flags:
| Bits | Meaning |
|---|---|
| 3-0 | The payload’s compression scheme: 0 = stored raw;
1 = a single LZMA stream (§2). |
| 4 | Trailing signature block (§4) present. 0 in this
version: no such block is present. |
| 7-5 | Reserved, 0 in this version. |
A reader checks magic and major_version
before parsing anything else. An unrecognized magic, or a
major_version the reader does not support, is a hard parse
failure reported at this point – never a crash or a best-effort guess
further into the file. A patch_version newer than the
reader knows about, and any flags bit the reader does not
recognize the meaning of, are not errors: both only ever signal an
optional, ignorable addition – a reader ignores whatever it does not
recognize and proceeds.
Payload
Immediately following the header:
| Field | Size | Value |
|---|---|---|
payload_length |
8 bytes, uint64 LE | Byte length of the payload that follows, as stored (its
LZMA-compressed size if flags selects lzma, or its raw byte
length otherwise). |
| Payload | payload_length bytes |
Raw tar bytes if flags selects none; a single LZMA
stream (§2) if flags selects lzma. Decoding it yields a tar
archive, of the shape given in §3. |
A reader that only wants the tar archive reads exactly
payload_length bytes at a fixed position (right after the
header and payload_length field) and never needs to read
further – regardless of whether a trailing signature block (§4) is
present, since that block always starts exactly at
payload_length bytes after the payload begins, a position
the reader already knows without inspecting anything past the payload
itself.
2. LZMA stream format
Where flags (§1) selects lzma, the payload’s stored
bytes are a raw LZMA stream: a 5-byte properties header
(lc/lp/pb packed into byte 0,
followed by a 4-byte little-endian dictionary size), followed by an
8-byte little-endian uncompressed-size field giving the decoded tar
archive’s byte length, followed by the LZMA-compressed data. This is
exactly what the public-domain LZMA SDK’s simple encoder
(LzmaCompress) produces. No
.xz/.7z framing or other container is layered
on top.
3. .scppkg contents
Decoding the payload (§1) yields a tar archive containing:
| Path | Contents |
|---|---|
MANIFEST.json |
This package’s manifest (below). |
any path a module’s interface names |
A nested, complete .scppm file (see The .scppm Module Interface
Format). |
any path a module’s archives[].path names |
A .scppa file: this module’s native archive of compiled
.scppo objects for one target triple. |
any path a module’s path names |
A raw .scpp interface source file
(kind: "source" modules only). |
MANIFEST.json:
{
"schema_version": "1.0",
"package": { "name": "mylib", "version": "1.2.0" },
"dependencies": [
{ "name": "otherlib", "version": "^1.0.0" }
],
"modules": {
"mylib.math": {
"kind": "binary",
"interface": "mylib.math.scppm",
"archives": [
{ "target_triple": "x86_64-linux-gnu",
"path": "mylib.math.x86_64-linux-gnu.scppa" }
],
"native_link_requirements": ["m"]
},
"mylib.collections": { "kind": "source", "path": "mylib.collections.scpp" }
}
}| Field | Meaning |
|---|---|
schema_version |
This manifest schema’s own "major.minor" version (§4),
independent of the envelope’s
major_version/patch_version (§1) and of any
nested .scppm’s own version. This document defines
"1.0". Read and validated before any other manifest
key. |
package.name |
This package’s name. Freeform. |
package.version |
This package’s version. Freeform. |
dependencies |
List of other packages this package needs (below). Author-provided
package-management metadata; this document does not specify how
version is matched or resolved. |
modules.<dotted-module-name>.kind |
"binary" (this entry has interface and
archives) or "source" (this entry has
path). |
modules.<dotted-module-name>.interface |
Present only if kind is "binary".
Tar-internal path of the nested .scppm file. |
modules.<dotted-module-name>.archives |
Present only if kind is "binary". List of
this module’s .scppa files, one per target triple
(below). |
modules.<dotted-module-name>.path |
Present only if kind is "source".
Tar-internal path of the raw .scpp interface source
file. |
modules.<dotted-module-name>.native_link_requirements |
System libraries (-l-style names) this module’s
compiled code depends on at link time. Author-provided; absent if
none. |
Each dependencies entry:
| Field | Meaning |
|---|---|
name |
The other package’s name. |
version |
The other package’s version requirement. Opaque to this document. |
Each archives entry:
| Field | Meaning |
|---|---|
target_triple |
The target triple this .scppa file was built for. |
path |
Tar-internal path of the .scppa file. |
A module built from several contributing files (its primary interface
unit, one or more implementation units, one or more partitions – see
ch11 §11.3
and §11.4)
still has exactly one archives entry per target triple: the
.scppa file itself is a native archive bundling however
many .scppo members that module needed for that triple, so
this manifest never has to enumerate them individually.
dependencies and native_link_requirements
are package-management metadata, not language-level facts: a
.scppm file carries neither, since a real C++ module has no
equivalent of either. dependencies is a single,
package-wide list – consistent with how other package ecosystems resolve
dependencies at package granularity, not per file or module – while
native_link_requirements is recorded per module, since
different modules’ compiled code may need different system
libraries.
A package may provide the same module as both
kind: "source" and (under a different entry) as
kind: "binary" for a consumer to choose between; this
document does not require the two, if both present, to agree on which
module names exist elsewhere in the package.
4. Extensibility
schema_version (§3) is a "major.minor"
string, versioning the manifest schema independently of the envelope’s
own major_version/patch_version (§1). A reader
checks its major component before trusting the rest of the manifest; a
major component the reader does not support is a hard parse failure,
reported before any other manifest key is read. A minor component newer
than the reader knows about is not an error – it only signals optional
fields the reader does not recognize, which it ignores before
proceeding. New optional MANIFEST.json fields are
introducible as a schema_version minor-version bump; a
breaking change to the schema – removing a field, or repurposing an
existing one – requires a major-version bump instead.
The envelope’s own
major_version/patch_version (§1) version the
byte container itself, independently of schema_version:
they govern the header/payload/signature framing described in §1, not
the manifest schema nested inside the payload.
A signature over the envelope’s header and payload together (so that
neither can be altered independently of the other) is not embedded
inside the payload’s own tar archive. It is a trailing block,
immediately following the payload and present only when
flags bit 4 (§1) is set:
| Field | Size | Value |
|---|---|---|
signature_length |
8 bytes, uint64 LE | Byte length of the signature that follows. |
| Signature | signature_length bytes |
Opaque to this document; covers the header and payload bytes together. |
Bit 4 is 0 in this version: no such block is present,
and the file ends immediately after the payload. Because a reader
locates the end of the payload from payload_length (§1)
alone, a reader that predates this block still correctly reads the
payload regardless of whether a signature block follows it.
Neither the signature’s own format, nor how a verifier establishes which key or certificate to trust, is specified by this document.