FBArtifactStaging
A macOS library that turns whatever a client sends to install — a local path, a URL, or an archive arriving on a stream — into a bundle on disk that a target can install.
The target frameworks only install what is already a bundle: ApplicationCommands.install(atPath:) takes an .app. Everything between the client's bytes and that path lives here, built on FBControlCore. idb_companion uses it for every install route, and it can be linked directly by other tools that install onto Simulators or Devices.
What it provides
- Installing from a source.
ApplicationCommands.install(from:)downloads, extracts and installs an application from anInstallSourcein one call, reporting what happens asInstallEvents: each stage's progress, how an archive was unpacked, and how a download went. - Staging.
Staging.installstages a source of any kind (application, test bundle, framework, dylib or dSYM), identifies theArtifactin it and hands that to the caller's install, removing whatever it unpacked afterwards. - Downloads. URL sources are fetched with
URLSessionand extracted as they arrive (a zip is spooled to disk first, since its index is at the end), with progress and aDownloadReportof the transfer. AConnectionOverridesends the connection to a given address instead of the one the URL's host resolves to, keeping the URL's host for TLS and HTTP. - Archive extraction. Tar (plain, gzip or zstd) and zip archives are extracted by sniffing the format from the first bytes rather than trusting what the client declared. Zip archives keep their file modes and symlinks, including when they arrive as a stream. Each format goes to whichever extractor is fastest for it (see Routes).
- Extracting a payload.
Staging.extractextracts a stream of files that is not an artifact, such as a push, through the same extractors, errors and events as an install. - Archive creation.
TarSource,GzippingSourceandArchiveOperationswrite the archives the companion sends back to clients, such as pulled files and test results.
How it fits together
Three layers, each depending only on the ones below it.
Staging/is the entry point.Staging.installtakes anInstallSourceand anArtifactKind, gets the source onto disk, identifies theArtifactin it, and calls the caller's install with it. Everything it unpacks lives in a temporary directory that is removed when the install returns, however it ends. It reports what happens as one ordered stream ofInstallEvents and fails with anInstallError.Downloads/fetches a.remoteURLand hands the bytes to extraction as they arrive, so the download and extract stages overlap.DataDownloadInputfetches it withURLSession. With aConnectionOverride,NetworkDownloadInputfetches it instead:URLSessiononly connects to the address a host resolves to, so it opens a Network framework connection to the override's address, verifies TLS against the URL's host, and reads the HTTP/1.1 response itself (HTTPResponseReader). It asks for an identity encoding, accepts only a200, and does not follow redirects, since following one would leave the override behind. Both report the same events, errors andDownloadReport.Archives/holds the byte sources, the extractors and the archive writers. Its extractors know nothing about installs: they read aByteSourceor a file and write a tree.
Byte sources
Every extractor reads a ByteSource, a blocking, pull-based reader. Extraction runs on its own thread, off Swift's cooperative pool, so a blocked read never starves other tasks. Bytes reach a source in one of three ways:
FileDescriptorSourcereads a file or a pipe, such as standard input.ChunkSourcebridges an asynchronous producer, such as the frames of an RPC, to the extractor's synchronous reads. It holds at mostcapacitybytes (8 MiB by default) plus one chunk, and the producer'spumpwaits for room until half of it is free. That holds a fast client to the extractor's pace without waking the producer on every read. A producer that fails reaches the reader asChunkSource.UpstreamFailure, never as a short archive. A reader that stops early callsabandon, and the producer then fails withChunkSource.ReaderClosedinstead of waiting for room that will not come.DownloadSpoolcarries a download.URLSessioncan't be held back: suspending a task does not stop what it has already read from arriving, and blocking its delegate queue starves the threads progress is reported on. So the download writes to an unlinked temporary file as fast as it arrives, and the reader follows behind on its own thread. A link faster than extraction puts what it is ahead by on disk rather than in memory.
Routes
SniffingExtractor reads the first bytes of a stream and picks an extractor for them. Each choice is reported as an ExtractionRoute: the format, and what extracted it.
| Format | Stream | Download |
|---|---|---|
| zip, or a zip compressed with zstd | extracted as it arrives, while being spooled to disk; repaired from the spooled copy's central directory (modes, symlinks), or extracted again from it if the stream reader can't handle the zip (zip_spool) | the same |
| tar compressed with zstd | in-process tar extractor (bsdtar on macOS can't decompress zstd) | the same |
| tar compressed with gzip | bsdtar | in-process tar extractor |
| plain tar | in-process tar extractor | the same |
For each format, the default is whichever extractor measured fastest. A download is read back from its spool file, and the in-process extractors read that faster, and with less CPU, than bsdtar for every format. The client's declared compression is ignored. ArchiveExtractors.default and ArchiveExtractors.inProcess are the two configurations; Benchmarks/ holds the benchmark that chooses between them.
Not every source is an archive. A directory is used where it is. A dylib or test run may arrive as the file itself, and a dylib may arrive gzipped. Any other file is taken to be an archive. Whatever is staged, if it doesn't hold an artifact of the kind being installed, fails as unidentifiable.
Errors
Failures are classified by stage, so a caller can tell the user what went wrong without matching on messages:
InstallErroris whatStagingthrows. For a download,httpStatus,transferFailed(with theDownloadReportof the transfer) andnotAnHTTPResponse.extractionFailedwhen the archive could not be extracted,noInstallableBundlewhen an extracted archive held no application, andunidentifiablewhen the staged source is not the kind it was installed as. When a download fails, it is reported as the transfer's failure, even though the extraction reading it fails too.ArchiveErroris the underlying error from an extractor:unsupported(another extractor may read it),corrupt,unsafePath(an entry that would land outside the extraction root or pass through a symlink), andextractorFailed(bsdtarexited unsuccessfully, with the tail of its standard error). File system failures arePOSIXErrors.ChunkSource.UpstreamFailureandChunkSource.ReaderClosedkeep a failing producer from being mistaken for a damaged archive, and an early-finishing reader from being mistaken for a client failure.idb_companionreports the latter as an aborted RPC.
Consumers
idb_companioninstalls applications, test bundles, frameworks, dylibs and dSYMs throughStaging.install, recordsInstallEvents as install telemetry, and pushes files throughStaging.extract. The contacts and media it is sent as tars in memory go throughTemporaryDirectory.withArchiveExtracted(_:).idb2installs throughApplicationCommands.install(from:), from a path, a URL, or standard input.staging-bench, inBenchmarks/, measures the throughput, CPU and memory of every route.