Establish unified Bun OS factory, app builds and package conversion

This commit is contained in:
aphrody-dev committed 2026-10-13 13:59:03 +02:00
commit 2e413aa6d9
65 files changed
+11621

No files matched your search

+15
View File
@@ -0,0 +1,15 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
indent_style = space
indent_size = 2
[*.rs]
indent_size = 4
[Makefile]
indent_style = tab
+5
View File
@@ -0,0 +1,5 @@
/out/
/*.local.json
/.factory.lock
/node_modules/
/target/
+50
View File
@@ -0,0 +1,50 @@
# Bun OS
Write project documentation in English. Follow the owner's requested language
for replies; the current request asks for French. Read GOAL.md, PLAN.md,
MEMORY.md and START-HERE.md before changes. Preserve concurrent work.
This checkout owns distribution assembly. The canonical runtime remains
aphrody-labs/bun on main in its existing checkout. Material owns the native
Material framework, tokens, libm3 and its WASM binding. COSMIC, libcosmic,
Linux, WSL, WSLg and APT retain their owners, histories and licenses.
Resolve paths from explicit configuration, the selected checkout or the
installed executable. Never hardcode a workstation checkout path in code.
Follow Linux kernel, Debian, COSMIC and Bun upstream conventions; read
docs/CODING.md. Use the installed Aphrody Bun runtime and its native tools.
Confirm APIs in primary sources. Use bun n2b for migration review and
native bun check, bun lint and bun fmt for authorized gates. Do not invoke
separate OXC, TypeScript, Node or Python commands for this workflow.
Use native Windows on the owner workstation; never start WSL there.
SSH, Docker and native hosts are distinct execution contexts. Use bun ssh
for the VPS. Bound long jobs asynchronously, retain PIDs/logs/receipts, and
use ConnectTimeout=15 and BatchMode=yes. Serialize heavy work through the
existing host lock. The Ubuntu factory keeps its state, socket, logs and
temporary files on its qualified build filesystem. Preserve active Shenron
restoration, services, sources, unpushed work and protected data.
The latest owner request authorizes an accurate session report followed by
consolidation commits and normal pushes of our owned paths. It does not
resume unrelated convergence work. During documentation preparation, do not
start builds, tests, deletions or commits. Native heavy work and local cleanup
remain subject to the explicit coordination hold; read the current owner
message before launching another job.
Keep authentication, raw provider sessions, cookies, databases, model weights
and protected .local stores isolated. Do not synchronize provider stores or
configure deprecated MCP aliases. Our measured 50 GB cleanup was on the SSH
VPS; it does not authorize workstation media or episode deletion.
Use Cloudflare MCP for account/domain mutations. aphrody-app is the existing
public edge owner for the application domains. Preserve the homepage, www,
existing services and R2 custom domains. A DNS/HTTPS response qualifies routing;
it does not qualify an independently deployed native application.
Reuse capability owners: exportPinnedSources for source export, buildApp for
app compilation, the canonical bun-cloudflare client for R2/authentication,
shared identity/contentIdentity for content, and package-converter ABI helpers.
Keep target/runtime and ABI versions distinct. Qualify source, binary, package,
desktop, image, WASM, performance and publication independently.
Do not add or run tests unless the owner requests them.
+45
View File
@@ -0,0 +1,45 @@
# Owner objective
Build an OS distribution with Bun as its application kernel, the COSMIC
desktop from cosmic-epoch, a cross-platform libcosmic fork, and an edition
of the owner reference “9937-389B”. Deliver Docker and WASM targets.
Create the requested OS integration branches while retaining the canonical
Bun runtime owner. Provide complete and performance editions with complete
locked Material and Bun fork sources.
Material owns the native Material 3 framework, its existing token and dynamic
color engine, libm3, the COSMIC adapter and the scalar WASM binding. Reuse
those owners for application/interface families. Preserve native control,
accessibility, focus, keyboard, layout and design-token semantics across
qualified web, native and COSMIC targets.
Integrate the existing Linux, WSL, WSLg and WSL kernel forks. Maintain the
official APT fork, Debian package/source support and apt.exe as a Windows
deliverable. Convert Ubuntu, COSMIC, Debian, MSYS2, MinGW and Cygwin packages
and sources into WinGet and Bun workflows, preserving dependencies, ABI,
provenance and licenses. Qualify the native libapt installation engine and
package transactions separately from the Bun conversion frontend.
Use the VPS for authenticated Ubuntu source retrieval and an OS build factory
following Ubuntu contributor guidance. Keep factory outputs on the qualified
build filesystem and preserve active work and protected data. The authorized
50 GB reclamation covers inventoried, disposable VPS classes.
Use glibc for Linux desktop editions and explicit native ABIs elsewhere.
Follow Linux, Debian, COSMIC and Bun conventions. Product documentation belongs
in README/docs, work status in PLAN.md, and session decisions in MEMORY.md.
Prepare unified deployment for the real applications in the owner's selected
workspace. Maintain their Cloudflare subdomains through Cloudflare MCP.
Compile complete standalone executables with the qualified Bun/runtime/native
owners. Store shared R2 assets by content identity, with app references to one
object per digest. Provision the chosen Rust deployment control plane and
self-hosted forge/workflow runners only after their host, authentication,
persistent storage and native job gates are satisfied.
Host hardware kernels remain platform-specific. A scalar libm3 WASM artifact
does not complete Bun/JSC/desktop WASM. Routing to the existing public Worker
does not complete app deployment. The full objective requires real source
exports, desktop/rootfs/images, runtime source-pin agreement, native libapt,
application execution and performance evidence. The owner reference
9937-389B must be identified before its edition can be qualified.
+24
View File
@@ -0,0 +1,24 @@
MIT License
Copyright (c) 2026 Yohan Pierre
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
The Rust libm3 crates are Apache-2.0 licensed as declared in their manifests.
Imported repositories retain their own licenses and notices.
+202
View File
@@ -0,0 +1,202 @@
Apache License
Version 2.0, January 2004
http://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
1. Definitions.
"License" shall mean the terms and conditions for use, reproduction,
and distribution as defined by Sections 1 through 9 of this document.
"Licensor" shall mean the copyright owner or entity authorized by
the copyright owner that is granting the License.
"Legal Entity" shall mean the union of the acting entity and all
other entities that control, are controlled by, or are under common
control with that entity. For the purposes of this definition,
"control" means (i) the power, direct or indirect, to cause the
direction or management of such entity, whether by contract or
otherwise, or (ii) ownership of fifty percent (50%) or more of the
outstanding shares, or (iii) beneficial ownership of such entity.
"You" (or "Your") shall mean an individual or Legal Entity
exercising permissions granted by this License.
"Source" form shall mean the preferred form for making modifications,
including but not limited to software source code, documentation
source, and configuration files.
"Object" form shall mean any form resulting from mechanical
transformation or translation of a Source form, including but
not limited to compiled object code, generated documentation,
and conversions to other media types.
"Work" shall mean the work of authorship, whether in Source or
Object form, made available under the License, as indicated by a
copyright notice that is included in or attached to the work
(an example is provided in the Appendix below).
"Derivative Works" shall mean any work, whether in Source or Object
form, that is based on (or derived from) the Work and for which the
editorial revisions, annotations, elaborations, or other modifications
represent, as a whole, an original work of authorship. For the purposes
of this License, Derivative Works shall not include works that remain
separable from, or merely link (or bind by name) to the interfaces of,
the Work and Derivative Works thereof.
"Contribution" shall mean any work of authorship, including
the original version of the Work and any modifications or additions
to that Work or Derivative Works thereof, that is intentionally
submitted to Licensor for inclusion in the Work by the copyright owner
or by an individual or Legal Entity authorized to submit on behalf of
the copyright owner. For the purposes of this definition, "submitted"
means any form of electronic, verbal, or written communication sent
to the Licensor or its representatives, including but not limited to
communication on electronic mailing lists, source code control systems,
and issue tracking systems that are managed by, or on behalf of, the
Licensor for the purpose of discussing and improving the Work, but
excluding communication that is conspicuously marked or otherwise
designated in writing by the copyright owner as "Not a Contribution."
"Contributor" shall mean Licensor and any individual or Legal Entity
on behalf of whom a Contribution has been received by Licensor and
subsequently incorporated within the Work.
2. Grant of Copyright License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
copyright license to reproduce, prepare Derivative Works of,
publicly display, publicly perform, sublicense, and distribute the
Work and such Derivative Works in Source or Object form.
3. Grant of Patent License. Subject to the terms and conditions of
this License, each Contributor hereby grants to You a perpetual,
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
(except as stated in this section) patent license to make, have made,
use, offer to sell, sell, import, and otherwise transfer the Work,
where such license applies only to those patent claims licensable
by such Contributor that are necessarily infringed by their
Contribution(s) alone or by combination of their Contribution(s)
with the Work to which such Contribution(s) was submitted. If You
institute patent litigation against any entity (including a
cross-claim or counterclaim in a lawsuit) alleging that the Work
or a Contribution incorporated within the Work constitutes direct
or contributory patent infringement, then any patent licenses
granted to You under this License for that Work shall terminate
as of the date such litigation is filed.
4. Redistribution. You may reproduce and distribute copies of the
Work or Derivative Works thereof in any medium, with or without
modifications, and in Source or Object form, provided that You
meet the following conditions:
(a) You must give any other recipients of the Work or
Derivative Works a copy of this License; and
(b) You must cause any modified files to carry prominent notices
stating that You changed the files; and
(c) You must retain, in the Source form of any Derivative Works
that You distribute, all copyright, patent, trademark, and
attribution notices from the Source form of the Work,
excluding those notices that do not pertain to any part of
the Derivative Works; and
(d) If the Work includes a "NOTICE" text file as part of its
distribution, then any Derivative Works that You distribute must
include a readable copy of the attribution notices contained
within such NOTICE file, excluding those notices that do not
pertain to any part of the Derivative Works, in at least one
of the following places: within a NOTICE text file distributed
as part of the Derivative Works; within the Source form or
documentation, if provided along with the Derivative Works; or,
within a display generated by the Derivative Works, if and
wherever such third-party notices normally appear. The contents
of the NOTICE file are for informational purposes only and
do not modify the License. You may add Your own attribution
notices within Derivative Works that You distribute, alongside
or as an addendum to the NOTICE text from the Work, provided
that such additional attribution notices cannot be construed
as modifying the License.
You may add Your own copyright statement to Your modifications and
may provide additional or different license terms and conditions
for use, reproduction, or distribution of Your modifications, or
for any such Derivative Works as a whole, provided Your use,
reproduction, and distribution of the Work otherwise complies with
the conditions stated in this License.
5. Submission of Contributions. Unless You explicitly state otherwise,
any Contribution intentionally submitted for inclusion in the Work
by You to the Licensor shall be under the terms and conditions of
this License, without any additional terms or conditions.
Notwithstanding the above, nothing herein shall supersede or modify
the terms of any separate license agreement you may have executed
with Licensor regarding such Contributions.
6. Trademarks. This License does not grant permission to use the trade
names, trademarks, service marks, or product names of the Licensor,
except as required for reasonable and customary use in describing the
origin of the Work and reproducing the content of the NOTICE file.
7. Disclaimer of Warranty. Unless required by applicable law or
agreed to in writing, Licensor provides the Work (and each
Contributor provides its Contributions) on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
implied, including, without limitation, any warranties or conditions
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
PARTICULAR PURPOSE. You are solely responsible for determining the
appropriateness of using or redistributing the Work and assume any
risks associated with Your exercise of permissions under this License.
8. Limitation of Liability. In no event and under no legal theory,
whether in tort (including negligence), contract, or otherwise,
unless required by applicable law (such as deliberate and grossly
negligent acts) or agreed to in writing, shall any Contributor be
liable to You for damages, including any direct, indirect, special,
incidental, or consequential damages of any character arising as a
result of this License or out of the use or inability to use the
Work (including but not limited to damages for loss of goodwill,
work stoppage, computer failure or malfunction, or any and all
other commercial damages or losses), even if such Contributor
has been advised of the possibility of such damages.
9. Accepting Warranty or Additional Liability. While redistributing
the Work or Derivative Works thereof, You may choose to offer,
and charge a fee for, acceptance of support, warranty, indemnity,
or other liability obligations and/or rights consistent with this
License. However, in accepting such obligations, You may act only
on Your own behalf and on Your sole responsibility, not on behalf
of any other Contributor, and only if You agree to indemnify,
defend, and hold each Contributor harmless for any liability
incurred by, or claims asserted against, such Contributor by reason
of your accepting any such warranty or additional liability.
END OF TERMS AND CONDITIONS
APPENDIX: How to apply the Apache License to your work.
To apply the Apache License to your work, attach the following
boilerplate notice, with the fields enclosed by brackets "[]"
replaced with your own identifying information. (Don't include
the brackets!) The text should be enclosed in the appropriate
comment syntax for the file format. We also recommend that a
file or class name and description of purpose be included on the
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright [yyyy] [name of copyright owner]
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
http://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.
+140
View File
@@ -0,0 +1,140 @@
# Project decisions and session evidence
## Owners and execution
- The selected distribution checkout was isolated from the canonical Bun runtime.
The owner moved our distribution and APT checkouts into the selected C:/src
layout after all agents paused local writes. Material is a separate owner.
Runtime discovery still resolves the actual installed executable and checkout.
- Project documentation is English; the owner requested French user replies.
Follow Linux, Debian, COSMIC and Bun conventions and native Bun orchestration.
No workstation WSL was started.
- Provider authentication, raw sessions, cookies, datasets, databases, model
weights and protected .local stores remain isolated. No provider synchronization
or workstation episode deletion was performed by our factory/app/domain tasks.
- The latest request authorizes accurate reporting and owned consolidation
commits/pushes. It does not resume unrelated earlier convergence work.
New heavy jobs/local cleanup are subject to the current coordination hold.
## Source topology and runtime
- The consolidation evidence snapshot has nine source roots, 83 mounts,
77 repository/revision pairs and 74 gitlinks. Repeated repositories with
different revisions retain separate source topology.
- The original four-root snapshot was 78 mounts and 71 revision pairs.
Later Linux/WSL/WSLg/kernel/APT roots and the libcosmic OS pin expanded it.
- The locked Bun source is c209b95eb0dcc964745f4684f127abb83cbe2697.
Selected Windows Bun.version is 1.4.4; Bun.revision is
2ae8fc65fba1d05c423a2433b1195835109942b6. Its native banner identifies the
development canary. The reported release URL does not authenticate an archive
or prove a build from the locked source.
- Material/os final consolidation 1e65a503420e4a8962b8ceaa18819eedbc280454
was normally pushed and independently checked by remote ref. The source lock
now pins that OS revision. Earlier ce47e50324ce4053c4fcd08be59db55254e231f7
remains the source of the qualified public scalar WASM artifact.
- libcosmic/os is pinned at 7f715bc5d2d383a070fd8c760aa52c5627df96b6.
COSMIC epoch retains its independently pinned children.
- The official APT GitHub mirror was archived. Our APT fork main was
fast-forwarded from live Salsa to 745fc126218c7cb3df48b22bbc7c7098fa2f7966;
the normal push and remote ref were verified. Ubuntu apt 3.2.0 source
provenance remains separate.
- Source export is consolidated under exportPinnedSources. Code gates cover
topology, committed files, LFS, protected roots, bounds and capacity checks.
No complete all-source export or LFS materialization receipt exists.
- An automatic review rejected recursive removal of the transient icons
metadata with “blocked by policy”. No bypass occurred; that read-only input
remains in ignored out/source-metadata/.
## Material correction stages
- Initial OS-local libm3/scalar ABI 1 was a duplicated baseline contract. Its
2061-byte artifact does not qualify the corrected owner and is superseded.
- libm3, libm3-wasm and the typed Bun loader moved into Material.
The existing native token engine owns HCT/CAM16, nine variants, 49 roles,
typography, shapes, motion, state and five adaptive breakpoints.
Native role order is unified through the no_std role owner.
- Corrected no_std/std and Windows COSMIC adapters compiled. Native dynamic
WASM ABI 2 built from ce47e503 to 113528 bytes, SHA256
c1abee38ea03c33b8150c14ef987906759182a6fd693c636b960922962c69ed8.
It executed with no host imports; green Primary changed ff65558f to ff1f6a4e.
Widths 599/840/1200 returned bar/collapsed/expanded rail.
- The richer existing M3 controls were transferred from clean Aphrody source
96e732f1c17591389e5bb6d2dc51cb9596edff72 into Material/crates/m3.
The transfer receipt preserves original and current file hashes; 19 of 23
Rust inputs were normalized to upstream default rustfmt. Initial compile
gates preceded formatting; root later rechecked the normalized integration.
No behavior tests or displayed GUI were run.
- libm3 now re-exports existing components, accessibility, forms, focus,
keyboard and composition under std. Root's later cached std/no_std checks
passed in 3.02/0.47 seconds; the COSMIC/MSVC facade check passed in 68 seconds.
The final normalized native CPU/no-native/COSMIC/WASM gates passed in
4.68/1.74/0.88/4.62 seconds. Complete metadata has 217 packages; the earlier
target-filtered graph had 197. Both resolve one local token 1.4.4 owner.
Material/docs/native-consolidation-2026-10-13.json records 79 source hashes.
- The original token consumer transition/publication is incomplete. A local
token package exists, but registry publisher authentication and package
gates were not established. Original clean inputs remain preserved until
the source/consumer transition qualifies.
- Web dynamic-color/MCU classic/HCT binding migration, complete component
coverage, displayed GUI, complete desktop and final facade WASM qualification
remain open.
## Ubuntu factory and cleanup
- Direct VPS APT retrieval authenticated signed Ubuntu InRelease, Sources and
every DSC/tar SHA256 using isolated lists/cache/tmp on the build filesystem.
Sources extracted: apt 3.2.0, dpkg 1.23.7ubuntu1, livecd-rootfs 26.04.35.
Missing detached developer keys for dpkg/livecd-rootfs were recorded; archive
authentication remained verified.
- Guarded remote cleanup observed 50,080,063,488 bytes reclaimed across 12
disposable classes, then stopped. No services/processes, Docker images,
containers or volumes were deleted by that batch. Active restore and factory
sources were excluded. Measurements credit per-action positive df changes,
not concurrent restore writes or a promised permanent free-space value.
- A dedicated build-disk Docker engine built a real Ubuntu 26.04.1 toolchain
image with glibc 2.43, sbuild/mmdebstrap/uidmap and image recipe tools.
The global Docker daemon and host package state were preserved.
- A clean sbuild unshare chroot produced livecd-rootfs 26.04.35 amd64:
148762 bytes, SHA256
7e12304e4bdcbd33526e28d7b6c1efa6e06b334a474bb58b3be476c60a89a836.
Build options were parallel=2 nocheck. /dev/console and /proc namespace
limitations were recorded; this is a DEB build, not a distribution rootfs
or complete OS image.
## Applications, packages and cloud
- One app compiler owns Bun.build/runtime snapshots/embedded files and reuses
the existing R2/authentication, digest and ABI owners. The APT builder is
a thin adapter with an actual native version qualifier.
- Final Windows apt.exe from the consolidated source lock/catalog is PE64 x64,
171469824 bytes, SHA256
43cacda8663021aa0640297978ef600b0d43bf8fe715d04a0f5464ee96bb2fbd.
It supersedes the earlier 994dd5f2 artifact.
It executed its version contract, inspected 926 authenticated APT source
archive entries and converted the source DSC plus a real MSYS2 zlib package.
Native libapt installation/package transactions remain absent.
- The consolidated apt.exe object was conditionally published under its SHA256
on dist.aphrody.app. Public HTTPS returned 200, exact bytes and SHA256, with
immutable one-year caching. Its matching WinGet installer URL is now available.
- The original zlib1.dll bytes are preserved: 127291 bytes, PE64 x64 library,
SHA256 11cd91171765cd65f1c3362e3df67e5f5a618a6b88c8cdec899b802eaf1c7b0d.
Publisher archive SHA matched; detached signature and DLL runtime execution
were not qualified. Native WinGet validated the portable APT manifest.
- XZ/bzip2 source members require an explicitly selected native 7-Zip decoder.
Gzip and bounded Zstandard use runtime APIs. Decoder dependency closure is
distinct from standalone Bun executable qualification.
- Cloudflare MCP created all 16 app domains on the existing aphrody-app Worker.
Every domain/DNS record was read back; public DNS resolved and HTTPS returned
200 for each. Homepage/www and assets/dist ownership remained unchanged.
No Worker code was uploaded and native apps remain undeployed.
- The public Material ABI 2 WASM object was uploaded once to
assets.aphrody.app/objects/sha256/<digest>, verified by SHA256, served HTTP 200,
and re-uploaded conditionally with created=false. Generic app asset pipelines
do not prove that all app asset catalogs are uploaded.
- Komodo/FerretDB/PostgreSQL DocumentDB/Forgejo/Runner are a prepared profile with
six immutable image pins and static Compose validation. No deployment contexts,
authenticated services, forge registration or runner jobs were qualified.
The [session report](docs/SESSION-REPORT-2026-10-13.md) holds exact artifact
and receipt references. Consolidation commit identities belong to Git log.
The full OS objective, performance measurements and 9937-389B remain incomplete.
+92
View File
@@ -0,0 +1,92 @@
# Bun OS plan
## Delivered evidence
- [x] Resolve and isolate distribution, runtime, Material and APT owners.
- [x] Define full/perf profiles and native/Docker/WASM target contracts.
- [x] Inventory nine source roots, 83 mounts, 77 repository/revision pairs and
74 recursive gitlinks at the consolidation evidence snapshot.
- [x] Implement one bounded source export owner with Git/LFS/integrity/capacity gates.
- [x] Move libm3/WASM into Material and reuse its existing native token engine.
- [x] Execute corrected Material scalar WASM ABI 2: nine variants, 49 native
roles, seed changes and bar/collapsed/expanded navigation.
- [x] Compile corrected no_std/std facades and native Windows COSMIC adapter.
- [x] Transfer the existing richer Material controls into Material/crates/m3
with input/output provenance; prior native/no-native/WASM checks passed.
- [x] Recheck normalized native CPU, no-native, COSMIC and WASM compilation
after the richer re-exports; one local token owner remains.
- [x] Publish Material/os consolidation 1e65a503 and refresh its source lock pin.
- [x] Publish the earlier Material/os artifact source ce47e503 and verify its
shared R2 WASM object, public HTTP 200 and conditional-write deduplication.
- [x] Pin Linux, WSL, WSLg, WSL kernel, libcosmic and current official APT owners.
- [x] Synchronize the official APT fork from live Salsa main.
- [x] Fetch/extract authenticated Ubuntu apt, dpkg and livecd-rootfs sources on VPS.
- [x] Reclaim 50,080,063,488 observed bytes from guarded disposable VPS classes.
- [x] Build a real isolated Ubuntu toolchain image on the build filesystem.
- [x] Build livecd-rootfs 26.04.35 into an amd64 DEB through a clean sbuild chroot.
- [x] Unify standalone app compilation, runtime identity, ABI and scoped asset owners.
- [x] Build/execute Windows apt.exe and convert the authenticated Ubuntu source
contract plus the original MSYS2 zlib DLL package.
- [x] Validate the matching portable WinGet manifest with native WinGet.
- [x] Publish the consolidated apt.exe to R2 and verify public bytes/SHA256 over HTTPS.
- [x] Inventory 16 real applications and create their Cloudflare Worker domains.
- [x] Verify managed DNS and public HTTPS HTTP 200 for all 16 application hosts.
- [x] Prepare Komodo/FerretDB/PostgreSQL/Forgejo/Runner deployment profile with
six immutable image pins and static Compose validation.
- [x] Consolidate the accurate session report and qualification boundaries.
## Remaining requirements
- [ ] Commit/push the owned OS consolidation batch; obtain final SHA from Git log.
- [ ] Migrate original Material consumers through the published Material owner;
the three unqualified Aphrody manifest changes were reverted, originals retained.
- [ ] Qualify the richer Material WASM facade after final source/format changes.
- [ ] Replace remaining web dynamic-color/MCU classic/HCT bridges with qualified
native bindings; complete component coverage and displayed GUI proof.
- [ ] Export all locked sources with complete inventories, measured capacity and
every LFS object; no complete source-export receipt exists yet.
- [ ] Rebuild/qualify the OS Bun runtime from its locked source revision.
- [ ] Build and qualify complete COSMIC/libcosmic desktops and hardware targets.
- [ ] Build a distribution rootfs and full/perf OS Docker images.
- [ ] Port native libapt to Windows and qualify signature trust/package transactions.
- [ ] Qualify complete conversion catalogs and runtime dependencies across all
requested Ubuntu/COSMIC/Debian/MSYS2/MinGW/Cygwin targets.
- [ ] Qualify remaining standalone native apps, desktop external context,
cross-target runtime snapshots and the shared Mach-O ABI owner.
- [ ] Activate the persistent control-plane/forge/runner contexts and live services.
- [ ] Qualify registered runner execution, authenticated origins and deployed apps.
- [ ] Complete Bun/JSC/desktop WASM.
- [ ] Measure performance, including the performance edition.
- [ ] Identify and integrate 9937-389B.
## Current limits
Final Material/os source is 1e65a503420e4a8962b8ceaa18819eedbc280454.
Normalized native CPU/no-native/COSMIC/WASM compile gates passed in
4.68/1.74/0.88/4.62 seconds. No GUI or unit tests were run.
The public Material WASM artifact is ABI 2, 113528 bytes, SHA256
c1abee38ea03c33b8150c14ef987906759182a6fd693c636b960922962c69ed8,
built from published Material/os ce47e50324ce4053c4fcd08be59db55254e231f7.
It does not qualify later facade source changes or a complete desktop.
The earlier 2061-byte ABI 1 contract is superseded.
The selected Windows Bun reports version 1.4.4 and revision
2ae8fc65fba1d05c423a2433b1195835109942b6; the locked OS runtime source is
c209b95eb0dcc964745f4684f127abb83cbe2697. The current binary is qualified for
the same-machine APT build/metadata contract, not as that locked OS runtime or
an authenticated stable release archive.
The livecd-rootfs DEB build succeeded with parallel=2/nocheck. Namespace
diagnostics reported absent /dev/console and failed /proc mounting.
Distribution rootfs/image/boot qualification is still open.
The 16 hosts route to the existing aphrody-app response. Independent native
apps remain undeployed. At the recorded 11:13 UTC certificate observation,
11 dedicated packs were active and five pending_validation; public TLS was
already valid for all hosts.
Heavy jobs were temporarily suspended during cross-thread storage recovery
coordination. Final scoped compiler gates and the APT rebuild then completed.
The remote 50 GB cleanup did not delete workstation episode media.
Full evidence, source/artifact hashes, publication state and deferred gates:
[session report](docs/SESSION-REPORT-2026-10-13.md).
+126
View File
@@ -0,0 +1,126 @@
# Bun OS
Bun OS is a development factory for assembling the Bun runtime fork, COSMIC
desktop and Material 3 into complete and performance distribution profiles.
Profile declarations describe the intended distribution; complete OS images
require their own source, runtime, desktop and image receipts.
| Profile | Desktop | Applications | Source payload |
| --- | --- | --- | --- |
| full | COSMIC | complete catalog | complete locked sources |
| perf | COSMIC | desktop core, files, terminal | complete locked sources |
Linux desktop targets use GNU ABI and glibc. Platform targets and features
are declared in distribution.json. Hardware kernels retain their native
Linux, Windows or Darwin owners.
## Distribution factory
```sh
bun src/cli.ts --help
bun src/cli.ts plan --profile full --target linux-x64
bun src/cli.ts sources --out ./out/full-new --config ./factory.local.json
bun src/cli.ts stage --out ./out/full-new --profile full --target linux-x64 --runtime ./ARTIFACT --receipt ./RECEIPT.json
bun src/cli.ts wasm --out ./out/full-new --config ./factory.local.json
bun src/cli.ts image --out ./out/full-new --profile full --target linux-x64 --tag bun-os:full
```
Configure existing checkouts and, when needed, a serialized native Cargo owner.
Paths resolve relative to the configuration. Without an explicit Bun path,
the factory uses APHRODY_BUN_CHECKOUT or locates the checkout containing the
installed executable.
The recursive source lock retains each submodule and revision-specific mount.
The single exporter checks committed files, exact Git topology, complete
inventories and materialized LFS content. Populated output paths are refused.
Acquisition requires measured sourceExport.estimates and adequate bytes/inodes.
See [the source export contract](docs/SOURCE-EXPORT.md).
Staging requires a complete source receipt and the exact locked runtime
revision, target, ABI, SHA256, byte count and native-runtime qualification.
Linux staging reads the actual glibc loader. A locally runnable Bun snapshot
with another revision can qualify an application build without qualifying
the locked OS runtime. Container recipes validate binary, source and libc
evidence; the Ubuntu toolchain image is separate from a shipped OS image.
## Material framework
The [Rust systems catalog](docs/RUST-SYSTEMS.md) records verified package
identities and placement within the current host and future boot architecture.
Material owns aphrody-m3-tokens, aphrody-m3, libm3 and libm3-wasm.
libm3 consumes the existing native color/design-token owner and the existing
control, accessibility, focus, keyboard, forms and composition models.
It does not define a second palette or control framework.
Dynamic seed colors, nine variants and contrast use the native token engine.
The portable no_std facade accepts computed colors. ABI 2 exposes 49 native
roles and distinguishes navigation bar, collapsed rail and expanded rail.
The typed loader is @aphrody/libm3/wasm in the Material workspace.
ABI 1 artifacts/loaders must not be substituted for ABI 2.
The WASM contract and native adapter compile gates are separate from displayed
GUI, browser component behavior and complete desktop qualification.
## Windows packages and sources
```sh
bun src/build-apt.ts --out ./out/apt-new
./out/apt-new/apt.exe --help
./out/apt-new/apt.exe inspect ./apt_3.2.0.dsc --format deb822 --ecosystem ubuntu
```
The thin APT build adapter calls the single app compiler. It embeds the
selected native Bun executable, records source/runtime/PE identities, and
executes the artifact's version contract. The frontend converts package
metadata, source contracts and qualified native PE payloads. It is distinct
from the official APT installation engine.
WinGet output requires an actual PE executable and explicit non-system runtime
owners. Native DLL archives instead produce native-assets metadata and retain
their original bytes, dependency mappings and licenses. ELF payloads retain
an explicit Windows rebuild contract.
Gzip and bounded Zstandard decoding use native runtime APIs. XZ and bzip2
members require an explicitly configured native 7-Zip decoder; that dependency
is not embedded in apt.exe. Detached signatures, DLL execution and package
transactions have separate qualification gates.
## Applications, routing and deployment
infra/apps.json inventories actual applications, source owners, target
snapshots and scoped asset roots. The single compiler bundles JavaScript,
embeds qualified native backends/assets and produces receipts. Missing native
backends, unresolved external context and unsupported ABI inspection fail the
independent-build gate. Shared asset objects use objects/sha256/<digest> once,
with per-app references.
The existing aphrody-app Worker owns public application hostnames.
DNS/HTTPS routing does not imply native application deployment.
The Rust deployment control and forge/workflow stack is a prepared profile;
activation requires qualified persistent contexts, credentials and live services.
- [Standalone builds and shared assets](docs/APP-BUILDS.md).
- [Public app domains](docs/APP-DOMAINS.md).
- [Deployment control and forge](docs/DEPLOYMENT-CONTROL.md).
- [Native source targets](docs/NATIVE-TARGETS.md).
- [Ubuntu factory](factory/README.md).
- [Image recipes](container/README.md).
## Layout
```text
src/ distribution and APT entry points
factory/ source export, Ubuntu and native target owners
infra/ app inventory, builds, assets and deployment profile
container/ complete/performance image recipes
packages/package-converter/ package and source conversion
sources.lock.json recursive immutable source topology
distribution.json profiles, target ABIs and features
```
Use the native Bun commands declared in package.json for authorized migration,
type, lint and format gates. Read [PLAN.md](PLAN.md) and the
[session report](docs/SESSION-REPORT-2026-10-13.md) for the current evidence
and incomplete requirements. Documentation consolidation does not complete
the OS objective.
+41
View File
@@ -0,0 +1,41 @@
# Start here
Read AGENTS.md, GOAL.md, PLAN.md and MEMORY.md.
The [session report](docs/SESSION-REPORT-2026-10-13.md) records the delivered
artifacts, correction stages and remaining qualification gaps.
This checkout assembles the distribution. Runtime changes belong to the
existing Bun checkout; the Material framework belongs to the configured
Material workspace. Other fork owners retain their repositories and licenses.
README.md contains product usage. PLAN.md distinguishes prepared recipes
from executed artifacts.
## Entry points
- Source topology: sources.lock.json and [source export](docs/SOURCE-EXPORT.md).
- Profiles and target ABIs: distribution.json.
- Distribution factory: src/cli.ts and src/distribution.ts.
- Ubuntu factory: factory/ubuntu.ts and factory/profile.json.
- Kernel/WSL/APT recipes: [native targets](docs/NATIVE-TARGETS.md).
- Package/source conversion: src/apt.ts and packages/package-converter/.
- Standalone compiler and assets: [app builds](docs/APP-BUILDS.md).
- Public routing: [app domains](docs/APP-DOMAINS.md).
- Control plane and workflows: [deployment control](docs/DEPLOYMENT-CONTROL.md).
Start with the native help and read-only plan:
```sh
bun src/cli.ts --help
bun src/cli.ts plan --profile full --target linux-x64
bun infra/app-build.ts plan apt --config infra/apps.json --target windows-x64
```
Use explicit existing checkout paths in a local configuration. Paths resolve
from that configuration; the runtime owner can also be discovered from the
selected executable. Choose fresh artifact outputs and the existing host lock.
Do not treat a snapshot's reported release URL as archive authentication or
as a build from the distribution's locked runtime revision.
Inspect permissions, running jobs, storage and the latest coordination hold
before starting a factory operation. Never start workstation WSL, confuse
the VPS with the local MCP context, or delete protected .local/provider stores.
+7
View File
@@ -0,0 +1,7 @@
{
"arrowParens": "avoid",
"printWidth": 120,
"trailingComma": "all",
"useTabs": false,
"quoteProps": "preserve"
}
+25
View File
@@ -0,0 +1,25 @@
# The allowlist accepts an assembled distribution only.
**
!Dockerfile
!.dockerignore
!distribution.json
!sources.receipt.json
!container/
!container/**
!runtime/
!runtime/bun
!runtime/bun-cosmic
!runtime/*.json
!sources/
!sources/**
!wasm/
!wasm/**
# Sources are exported from Git's tracked inventory by the staging owner.
# Preserve tracked fixture paths, including node_modules, PEM and dotenv files.
# Provider stores and Git administration are never distribution source.
**/.git
**/.git/**
**/.codex/sessions
**/.codex/sessions/**
**/.claude/projects
**/.claude/projects/**
+83
View File
@@ -0,0 +1,83 @@
# syntax=docker/dockerfile:1
# Build from `bun-distribution stage` output, never from a live checkout.
FROM ubuntu:26.04 AS base
ENV DEBIAN_FRONTEND=noninteractive LANG=C.UTF-8
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates libgcc-s1 libstdc++6 \
&& rm -rf /var/lib/apt/lists/* \
&& groupadd --gid 10000 bun \
&& useradd --uid 10000 --gid 10000 --create-home --shell /usr/sbin/nologin bun \
&& install -d -m 0700 -o bun -g bun /run/user/10000 \
&& install -d -o bun -g bun /work
FROM base AS payload
COPY runtime/ /opt/bun-os/runtime/
COPY sources/ /opt/bun-os/sources/
COPY wasm/ /opt/bun-os/wasm/
COPY distribution.json /opt/bun-os/distribution.json
COPY sources.receipt.json /opt/bun-os/sources.receipt.json
COPY container/ /opt/bun-os/container/
RUN chmod 0755 /opt/bun-os/runtime/bun \
&& ln -s /opt/bun-os/runtime/bun /usr/local/bin/bun \
&& if [ -f /opt/bun-os/runtime/bun-cosmic ]; then \
chmod 0755 /opt/bun-os/runtime/bun-cosmic \
&& ln -s /opt/bun-os/runtime/bun-cosmic /usr/local/bin/bun-cosmic; fi \
&& bun /opt/bun-os/container/verify-payload.ts \
&& getconf GNU_LIBC_VERSION > /opt/bun-os/runtime/libc-version.txt \
&& ldd /usr/local/bin/bun > /opt/bun-os/runtime/shared-libraries.txt \
&& ! grep -q 'not found' /opt/bun-os/runtime/shared-libraries.txt
FROM base AS cosmic-core
COPY --from=payload /usr/local/bin/ /usr/local/bin/
COPY --from=payload /opt/bun-os/ /opt/bun-os/
# Bootstrap the signed official Pop repository with its source-pinned public key.
RUN apt-get update \
&& apt-get install -y --no-install-recommends curl \
&& curl --fail --silent --show-error --location --connect-timeout 15 --max-time 60 \
https://raw.githubusercontent.com/pop-os/keyring/356a75e405d00a23889998d2e8f2508a9e67b535/usr/share/keyrings/pop-archive-keyring.gpg \
--output /usr/share/keyrings/pop-archive-keyring.gpg \
&& echo 'a228ba8145acf8c57afd44ad7e8eb2d11776b5a2ca5fe5174698110023952f07 /usr/share/keyrings/pop-archive-keyring.gpg' | sha256sum --check - \
&& echo 'deb [signed-by=/usr/share/keyrings/pop-archive-keyring.gpg] https://apt.pop-os.org/release resolute main' > /etc/apt/sources.list.d/pop-os-release.list \
&& printf '#!/bin/sh\nexit 101\n' > /usr/sbin/policy-rc.d \
&& chmod 0755 /usr/sbin/policy-rc.d \
&& apt-get update \
&& bun /opt/bun-os/container/install-cosmic.ts perf \
&& apt-get install -y --no-install-recommends \
dbus-x11 mesa-vulkan-drivers libgl1-mesa-dri libegl-mesa0 pipewire pipewire-pulse wireplumber \
&& apt-get purge -y curl \
&& rm -rf /var/lib/apt/lists/* /tmp/bun-os-cosmic-debs \
&& BUN_OS_PROFILE=perf bun /opt/bun-os/container/entrypoint.ts health
ENV HOME=/home/bun BUN_OS_ROOT=/opt/bun-os \
XDG_RUNTIME_DIR=/run/user/10000 XDG_CURRENT_DESKTOP=COSMIC XDG_SESSION_DESKTOP=COSMIC
FROM cosmic-core AS perf
ARG DISTRIBUTION_VERSION
ARG SOURCE_REVISION
LABEL org.opencontainers.image.title="Bun OS perf COSMIC" \
org.opencontainers.image.source="https://github.com/aphrody-labs/bun" \
org.opencontainers.image.version=$DISTRIBUTION_VERSION \
org.opencontainers.image.revision=$SOURCE_REVISION
ENV BUN_OS_PROFILE=perf
RUN dpkg-query -W -f='${Package}\t${Version}\n' > /opt/bun-os/runtime/os-packages.tsv
WORKDIR /work
USER 10000:10000
ENTRYPOINT ["/usr/local/bin/bun", "/opt/bun-os/container/entrypoint.ts"]
CMD ["cosmic"]
FROM cosmic-core AS full
ARG DISTRIBUTION_VERSION
ARG SOURCE_REVISION
LABEL org.opencontainers.image.title="Bun OS full COSMIC" \
org.opencontainers.image.source="https://github.com/aphrody-labs/bun" \
org.opencontainers.image.version=$DISTRIBUTION_VERSION \
org.opencontainers.image.revision=$SOURCE_REVISION
RUN apt-get update \
&& bun /opt/bun-os/container/install-cosmic.ts full \
&& dpkg-query -W -f='${Package}\t${Version}\n' > /opt/bun-os/runtime/os-packages.tsv \
&& rm -rf /var/lib/apt/lists/* /tmp/bun-os-cosmic-debs \
&& BUN_OS_PROFILE=full bun /opt/bun-os/container/entrypoint.ts health
ENV BUN_OS_PROFILE=full
WORKDIR /work
USER 10000:10000
ENTRYPOINT ["/usr/local/bin/bun", "/opt/bun-os/container/entrypoint.ts"]
CMD ["cosmic"]
+58
View File
@@ -0,0 +1,58 @@
# Bun OS images
Both image profiles use the Linux GNU/glibc runtime and Ubuntu26.04 base.
perf includes the COSMIC session, compositor, panel, settings, launcher,
services, file manager and terminal. full adds the application catalog.
Both retain complete source payloads and licenses.
Build from a factory-staged context. BUN_OS_STAGE identifies the context;
SOURCE_REVISION and DISTRIBUTION_VERSION come from its receipt.
```sh
docker build --target perf --file "$BUN_OS_STAGE/container/Dockerfile" --build-arg SOURCE_REVISION --build-arg DISTRIBUTION_VERSION --tag bun-os:perf "$BUN_OS_STAGE"
docker build --target full --file "$BUN_OS_STAGE/container/Dockerfile" --build-arg SOURCE_REVISION --build-arg DISTRIBUTION_VERSION --tag bun-os:full "$BUN_OS_STAGE"
docker run --rm bun-os:full health
docker run --rm bun-os:perf --version
docker run --rm --entrypoint /usr/local/bin/bun bun-os:perf --revision
```
The context contains runtime/bun, sources/, sources.receipt.json,
distribution.json, container/ and wasm/. The optional bun-cosmic helper belongs
under runtime/. Private stores and live checkout contents must not enter staging.
The runtime receipt identifies repository, source revision, GNU ABI, target,
version, SHA256, bytes and a passed native-runtime gate. The image validates
the actual executable and records the deployed GNU libc version and shared
library resolution.
cosmic-packages.json pins package versions, URLs and checksums. APT uses the
signed official Pop repository and a source-pinned public key. A package lock
is architecture-specific; provide an ARM64 lock before building that target.
## Desktop session
The session runs cosmic-session cosmic-comp on a private D-Bus bus. A Linux
Wayland host supplies an explicitly authorized display socket:
```sh
docker run --rm --init \
--user "$BUN_OS_UID:$BUN_OS_GID" \
--security-opt no-new-privileges --cap-drop ALL \
--tmpfs "/run/bun-os:rw,nosuid,noexec,size=16m,mode=0700,uid=$BUN_OS_UID,gid=$BUN_OS_GID" \
--tmpfs "/home/bun:rw,nosuid,size=128m,mode=0700,uid=$BUN_OS_UID,gid=$BUN_OS_GID" \
--mount "type=bind,source=$BUN_OS_WAYLAND_SOCKET,target=/run/host-wayland,readonly" \
--env XDG_RUNTIME_DIR=/run/bun-os --env WAYLAND_DISPLAY=/run/host-wayland \
--env COSMIC_BACKEND=winit bun-os:full cosmic
```
Hardware rendering requires authorized render nodes and groups on that host.
KMS requires explicit device and seat configuration. health checks component
availability; graphical qualification requires an actual desktop session.
The entrypoint forwards SIGINT, SIGTERM and exit status. exec runs an executable
with arguments. --entrypoint /usr/local/bin/bun selects the direct native runtime.
References: [COSMIC](https://github.com/pop-os/cosmic-epoch),
[Pop package index](https://apt.pop-os.org/release/dists/resolute/main/binary-amd64/Packages),
[repository key](https://github.com/pop-os/keyring/tree/356a75e405d00a23889998d2e8f2508a9e67b535),
[COSMIC backend](https://github.com/pop-os/cosmic-comp/blob/41497b42d9744d9963c4c6d17e8add49489b45ee/src/backend/mod.rs).
+195
View File
@@ -0,0 +1,195 @@
{
"schema": 1,
"architecture": "amd64",
"repository": "https://apt.pop-os.org/release",
"suite": "resolute",
"retrievedAt": "2026-10-13",
"packages": [
{
"name": "cosmic-app-library",
"version": "1.10.0~1791324907~26.04~1107375",
"path": "pool/resolute/cosmic-app-library/1107375789e3f60e120b6dc5b81379771ecaa811/cosmic-app-library_1.10.0~1791324907~26.04~1107375_amd64.deb",
"sha256": "20f060649ea9b0aa0bf0022188feeed44a0b28576e19456838ef4e217346a007"
},
{
"name": "cosmic-applets",
"version": "1.10.0~1791312291~26.04~406d1b7",
"path": "pool/resolute/cosmic-applets/406d1b7d627138fdf4078f654268d3d8774e50f1/cosmic-applets_1.10.0~1791312291~26.04~406d1b7_amd64.deb",
"sha256": "2c04e94373b50e8a483b3b8d63154fdf8ade46bac303e00e1dba8caf6e93845e"
},
{
"name": "cosmic-bg",
"version": "1.10.0~1791316428~26.04~91d525b",
"path": "pool/resolute/cosmic-bg/91d525b7310f5a497c9fd09a25d2710eb9f8a6f7/cosmic-bg_1.10.0~1791316428~26.04~91d525b_amd64.deb",
"sha256": "c772f424e5e43d539c2f6fcfd39ee805aa07468b27ad92ec9531f9bb0c5e1ce2"
},
{
"name": "cosmic-comp",
"version": "0.1~1791227611~26.04~41497b4",
"path": "pool/resolute/cosmic-comp/41497b42d9744d9963c4c6d17e8add49489b45ee/cosmic-comp_0.1~1791227611~26.04~41497b4_amd64.deb",
"sha256": "3283fef8acbe265585512feb6555925e0d35cca6558186c5e3123e6015014d57"
},
{
"name": "cosmic-edit",
"version": "1.10.0~1791316546~26.04~5ddf511",
"path": "pool/resolute/cosmic-edit/5ddf5115995e02fd71851a42bf0e6a9c9b5a1ce5/cosmic-edit_1.10.0~1791316546~26.04~5ddf511_amd64.deb",
"sha256": "81ad741db26aae02fc9b29c35b9796a3d6d540b8cfc5b960b2da66508c2cfc7e"
},
{
"name": "cosmic-files",
"version": "1.10.0~1791316382~26.04~ac5cef7",
"path": "pool/resolute/cosmic-files/ac5cef76fa60b5a329bfda3d30825489ec28af53/cosmic-files_1.10.0~1791316382~26.04~ac5cef7_amd64.deb",
"sha256": "2ce45d73583501fbe291327230213b8c9835539c9bc1d7c0f5f4903d4397a27f"
},
{
"name": "cosmic-greeter-daemon",
"version": "1.10.0~1791400742~26.04~98df07d",
"path": "pool/resolute/cosmic-greeter/98df07df93df1cb091c95451f7162dca1a2ae2dd/cosmic-greeter-daemon_1.10.0~1791400742~26.04~98df07d_amd64.deb",
"sha256": "fb7f6e60a59036f1389b88617c731a44fcc9d907f37e3de7ec5c1e5c384e2285"
},
{
"name": "cosmic-greeter",
"version": "1.10.0~1791400742~26.04~98df07d",
"path": "pool/resolute/cosmic-greeter/98df07df93df1cb091c95451f7162dca1a2ae2dd/cosmic-greeter_1.10.0~1791400742~26.04~98df07d_amd64.deb",
"sha256": "172308ae2a0801e1bacbb752b873a26e33453c5469f43b1f70b101e3f73430b3"
},
{
"name": "cosmic-icons",
"version": "0.1.0~1790783741~26.04~f1a282a",
"path": "pool/resolute/cosmic-icons/f1a282a0822aed1f0083dd2ab2e6c65bd5b401eb/cosmic-icons_0.1.0~1790783741~26.04~f1a282a_amd64.deb",
"sha256": "f249cf9ee5c1d735eb0e9e5c1e0431ea3966ab07783da37b16ce9e7b259cd796"
},
{
"name": "cosmic-idle",
"version": "1.10.0~1791316310~26.04~21e983c",
"path": "pool/resolute/cosmic-idle/21e983c63e75640ab0055495e768a5c616742c19/cosmic-idle_1.10.0~1791316310~26.04~21e983c_amd64.deb",
"sha256": "dcf0b9ffbeddc35fe50097a4a065ae9bcbb669e9c51b2f266f4dc798632177f8"
},
{
"name": "cosmic-initial-setup",
"version": "1.10.0~1791314237~26.04~8b025bc",
"path": "pool/resolute/cosmic-initial-setup/8b025bc9eec4844d8c01fb238e5298b3db920b3f/cosmic-initial-setup_1.10.0~1791314237~26.04~8b025bc_amd64.deb",
"sha256": "b7547a2a77bfbf6321a3bb847ac4373325cabced3c6307eb2c45917be8a3f85b"
},
{
"name": "cosmic-launcher",
"version": "1.10.0~1791390530~26.04~03195dd",
"path": "pool/resolute/cosmic-launcher/03195ddb097c6ad83c4072917d60d900b8bd944f/cosmic-launcher_1.10.0~1791390530~26.04~03195dd_amd64.deb",
"sha256": "cb196180cef376fc3f8b4840793d9e28de37ec2bc534f5e829124655defc0549"
},
{
"name": "cosmic-monitor",
"version": "1.10.0~1791316617~26.04~a79ce9e",
"path": "pool/resolute/cosmic-monitor/a79ce9e7d2259a82150d56978fb7a094bd4ec2f0/cosmic-monitor_1.10.0~1791316617~26.04~a79ce9e_amd64.deb",
"sha256": "a826cfa924bd38dbf4081fdf5aa472d21e9af98fca8fb3aa44573ad5e5e81e68"
},
{
"name": "cosmic-notifications",
"version": "1.10.0~1791314632~26.04~1e4c1f1",
"path": "pool/resolute/cosmic-notifications/1e4c1f11fdcae273d6954fd0bfc54749a6145267/cosmic-notifications_1.10.0~1791314632~26.04~1e4c1f1_amd64.deb",
"sha256": "391d518373fcd3fa50e88b148c45fdfce69c57e466c494166de5587049caed3b"
},
{
"name": "cosmic-osd",
"version": "1.10.0~1791315686~26.04~9b14ee0",
"path": "pool/resolute/cosmic-osd/9b14ee017fb8fdb7d03d73da597cf17b89d890f7/cosmic-osd_1.10.0~1791315686~26.04~9b14ee0_amd64.deb",
"sha256": "30322816885469574ad081b7db0f439e9184099d40c07369601ea9b3389f597f"
},
{
"name": "cosmic-osk",
"version": "1.10.0~1791390565~26.04~1b7ec69",
"path": "pool/resolute/cosmic-osk/1b7ec698b79d248d6a59ff426f453fc7ce3edc91/cosmic-osk_1.10.0~1791390565~26.04~1b7ec69_amd64.deb",
"sha256": "116e0694836e6474a6d1f02f3a30d42f6aa3c812912076e210e773a2d80dd318"
},
{
"name": "cosmic-panel",
"version": "1.10.0~1791315703~26.04~3ce6ce1",
"path": "pool/resolute/cosmic-panel/3ce6ce1cec97669f91e809962d9446aaefe8a06e/cosmic-panel_1.10.0~1791315703~26.04~3ce6ce1_amd64.deb",
"sha256": "e5de9b5e80751b354ec35e35f5a80c8c5a4dd6029a7546c6622e5177ed3bcaa4"
},
{
"name": "cosmic-player",
"version": "1.10.0~1791315619~26.04~9004537",
"path": "pool/resolute/cosmic-player/900453787f999ec151b8d58683923e3110b727e1/cosmic-player_1.10.0~1791315619~26.04~9004537_amd64.deb",
"sha256": "18a39557aa1ba1f3452837949164ec62b9d8b246935ca775629edc2e8328da5c"
},
{
"name": "cosmic-randr",
"version": "1.10.0~1791314440~26.04~85efc7f",
"path": "pool/resolute/cosmic-randr/85efc7febb99876094cf90af6dbce0927ad8cca7/cosmic-randr_1.10.0~1791314440~26.04~85efc7f_amd64.deb",
"sha256": "5dfb398109d1eea65781d688497f20e8b5e48d35bb98757bbeef0f756e015f9b"
},
{
"name": "cosmic-reader",
"version": "0.1.0~1778600608~26.04~0dde6ae",
"path": "pool/resolute/cosmic-reader/0dde6aed2c51b16e48d2756ceeb0c92620442418/cosmic-reader_0.1.0~1778600608~26.04~0dde6ae_amd64.deb",
"sha256": "7dacc717be4db42b75ffdca341aae89a0819d55e35eafc14e0a294976a96bff1"
},
{
"name": "cosmic-screenshot",
"version": "1.10.0~1791314515~26.04~07e2758",
"path": "pool/resolute/cosmic-screenshot/07e27587eb387ecf38cb4692ccbf25ad20760d22/cosmic-screenshot_1.10.0~1791314515~26.04~07e2758_amd64.deb",
"sha256": "7d63a857b92e3cd78227887b09b179021278265af6efcae2a27861b000b1c030"
},
{
"name": "cosmic-session",
"version": "1.10.0~1791314577~26.04~8093b59",
"path": "pool/resolute/cosmic-session/8093b59bb1262b0612d814e6540ba936303f735a/cosmic-session_1.10.0~1791314577~26.04~8093b59_amd64.deb",
"sha256": "58d2e71595c7e00b370a24f171e69437c4f079a61e695a06c60da901445b06ff"
},
{
"name": "cosmic-settings-daemon",
"version": "1.10.0~1791315582~26.04~9e4ae69",
"path": "pool/resolute/cosmic-settings-daemon/9e4ae69fc175191f15789d35423806ea0f093c9d/cosmic-settings-daemon_1.10.0~1791315582~26.04~9e4ae69_amd64.deb",
"sha256": "4d99de4ac97e516fedf34167c9f9e9df48ec39325a02fae094abda9aed00b30d"
},
{
"name": "cosmic-settings",
"version": "1.10.0~1791394725~26.04~58aeb65",
"path": "pool/resolute/cosmic-settings/58aeb65244b8830880e8b6ced89dcd25663f20b1/cosmic-settings_1.10.0~1791394725~26.04~58aeb65_amd64.deb",
"sha256": "28f578146f6ead59c8ba8013fe1aa9870d3520c0ddda5107af9c6a7c7289f1fa"
},
{
"name": "cosmic-store",
"version": "1.10.0~1791381777~26.04~c7b36a2",
"path": "pool/resolute/cosmic-store/c7b36a2fdccf1ce88944793af67c3664413ee560/cosmic-store_1.10.0~1791381777~26.04~c7b36a2_amd64.deb",
"sha256": "246bc4cad1ff06c81f1ba93c2bc98263a1d7c70758d8c6d35a0689ff9c89a9e9"
},
{
"name": "cosmic-term",
"version": "1.10.0~1791316259~26.04~3bfca1e",
"path": "pool/resolute/cosmic-term/3bfca1eb804de2f069e62d0709982beca908cb01/cosmic-term_1.10.0~1791316259~26.04~3bfca1e_amd64.deb",
"sha256": "fa20b3bb46b52dac7b6a8e300adcf49423f6583d4baeea6fc75a4dd666e6229c"
},
{
"name": "cosmic-viewer",
"version": "1.10.0~1791390582~26.04~77ba62d",
"path": "pool/resolute/cosmic-viewer/77ba62deb7027217efab1999e90670f3c89c5a02/cosmic-viewer_1.10.0~1791390582~26.04~77ba62d_amd64.deb",
"sha256": "8775a3f5df6f7e66b7ac4e3f4b41dfb76fa53503a18d8d6f498ac51b79a420a5"
},
{
"name": "cosmic-wallpapers",
"version": "1.0.0~1790783798~26.04~da595bb",
"path": "pool/resolute/cosmic-wallpapers/da595bba495cb8fc57f26505621fc25000218349/cosmic-wallpapers_1.0.0~1790783798~26.04~da595bb_all.deb",
"sha256": "3d3e8e1780fdeaeae7fe841ac9c991a360ad8c53c389ca1e9f4239416770aa41"
},
{
"name": "cosmic-workspaces",
"version": "1.0.12~1790783807~26.04~cb953ea",
"path": "pool/resolute/cosmic-workspaces-epoch/cb953ea0e6fb45ac8f62f9c52f1347c971388e03/cosmic-workspaces_1.0.12~1790783807~26.04~cb953ea_amd64.deb",
"sha256": "80af3ea10c1bfc3f8b20101277b405c81889e248153e3df80b6652597a09e9ea"
},
{
"name": "pop-launcher",
"version": "1.2.6~1790783815~26.04~8a09fa5",
"path": "pool/resolute/launcher/8a09fa50a9fbbe66a97afe3865b5e61dd1d0432c/pop-launcher_1.2.6~1790783815~26.04~8a09fa5_amd64.deb",
"sha256": "0272e0bbf42826d9ca51b7a0adabdfec1b563382bb75b62fc82c6fa3c426b532"
},
{
"name": "xdg-desktop-portal-cosmic",
"version": "1.10.0~1791324839~26.04~9d35e63",
"path": "pool/resolute/xdg-desktop-portal-cosmic/9d35e6300b69cf06da723a502a9a56604a022786/xdg-desktop-portal-cosmic_1.10.0~1791324839~26.04~9d35e63_amd64.deb",
"sha256": "dbf68796c8dbb881e74b612a94017d89498cbbb01da1e0639ddfc0da95196880"
}
]
}
+63
View File
@@ -0,0 +1,63 @@
import { verifyPayload } from "./verify-payload";
const [mode = "cosmic", ...args] = Bun.argv.slice(2);
const root = Bun.env.BUN_OS_ROOT ?? "/opt/bun-os";
const desktopCommands = [
"cosmic-session",
"cosmic-comp",
"cosmic-panel",
"cosmic-settings",
"cosmic-settings-daemon",
"cosmic-app-library",
"cosmic-launcher",
"cosmic-workspaces",
"cosmic-notifications",
"cosmic-bg",
"cosmic-files",
"cosmic-term",
];
const fullApplications = ["cosmic-edit", "cosmic-player", "cosmic-viewer", "cosmic-store"];
function requireDesktop() {
const commands = Bun.env.BUN_OS_PROFILE === "full" ? [...desktopCommands, ...fullApplications] : desktopCommands;
for (const command of commands) {
if (!Bun.which(command)) throw new Error(`Missing COSMIC component: ${command}`);
}
}
if (mode === "health") {
const receipt = await verifyPayload(root);
// During image construction BUN_OS_PROFILE is not set yet.
if (Bun.env.BUN_OS_PROFILE === "full" || Bun.which("cosmic-session")) requireDesktop();
console.log(
JSON.stringify({
name: receipt.name,
revision: receipt.runtime.revision,
desktop: !!Bun.which("cosmic-session"),
}),
);
} else {
let command: string[];
if (mode === "cosmic") {
requireDesktop();
if (!Bun.env.WAYLAND_DISPLAY && !Bun.env.DISPLAY && Bun.env.COSMIC_BACKEND !== "kms") {
throw new Error("COSMIC needs a mounted Wayland/X11 display, or an explicitly configured KMS seat");
}
// COSMIC's own session starts the compositor, panel, launcher and daemons.
command = ["dbus-run-session", "--", "cosmic-session", "cosmic-comp", ...args];
} else if (mode === "exec") {
if (!args.length) throw new Error("exec requires an executable and its arguments");
command = args;
} else {
command = ["/usr/local/bin/bun", mode, ...args];
}
const child = Bun.spawn(command, { stdin: "inherit", stdout: "inherit", stderr: "inherit" });
const terminate = () => child.kill("SIGTERM");
const interrupt = () => child.kill("SIGINT");
process.on("SIGTERM", terminate);
process.on("SIGINT", interrupt);
const exitCode = await child.exited;
process.off("SIGTERM", terminate);
process.off("SIGINT", interrupt);
process.exit(exitCode);
}
+63
View File
@@ -0,0 +1,63 @@
import { $ } from "bun";
type CosmicPackages = {
architecture: string;
repository: string;
packages: { name: string; version: string; path: string; sha256: string }[];
};
const root = Bun.env.BUN_OS_ROOT ?? "/opt/bun-os";
const lock = (await Bun.file(`${root}/container/cosmic-packages.json`).json()) as CosmicPackages;
const profile = Bun.argv[2] ?? "full";
if (profile !== "perf" && profile !== "full") throw new Error("Unknown COSMIC profile");
const corePackages = new Set([
"cosmic-session",
"cosmic-comp",
"cosmic-panel",
"cosmic-applets",
"cosmic-app-library",
"cosmic-launcher",
"cosmic-settings",
"cosmic-settings-daemon",
"cosmic-workspaces",
"cosmic-notifications",
"cosmic-bg",
"cosmic-osd",
"cosmic-idle",
"cosmic-randr",
"cosmic-files",
"cosmic-greeter",
"cosmic-greeter-daemon",
"cosmic-osk",
"cosmic-screenshot",
"cosmic-term",
"cosmic-icons",
"pop-launcher",
"xdg-desktop-portal-cosmic",
]);
const packages = profile === "full" ? lock.packages : lock.packages.filter(entry => corePackages.has(entry.name));
if (profile === "perf" && packages.length !== corePackages.size) throw new Error("Missing pinned core COSMIC package");
if (process.arch !== "x64" || lock.architecture !== "amd64") {
throw new Error("The COSMIC package lock currently provides Linux x64 only");
}
const directory = "/tmp/bun-os-cosmic-debs";
await $`mkdir -p ${directory}`;
const files: string[] = [];
for (const entry of packages) {
const response = await fetch(`${lock.repository}/${entry.path}`, {
signal: AbortSignal.timeout(120_000),
});
if (!response.ok) throw new Error(`Downloading ${entry.name} returned ${response.status}`);
const data = await response.arrayBuffer();
const sha256 = new Bun.CryptoHasher("sha256").update(data).digest("hex");
if (sha256 !== entry.sha256) throw new Error(`Checksum mismatch for ${entry.name}`);
const path = `${directory}/${entry.name}.deb`;
await Bun.write(path, data);
files.push(path);
}
await $`apt-get install -y --no-install-recommends ${files}`;
for (const entry of packages) {
const version = (await $`dpkg-query -W -f=${"${Version}"} ${entry.name}`.text()).trim();
if (version !== entry.version) throw new Error(`Installed ${entry.name}: expected ${entry.version}, got ${version}`);
}
console.log(`Installed ${packages.length} exact COSMIC packages for ${profile}`);
+38
View File
@@ -0,0 +1,38 @@
import { readdir, readlink } from "node:fs/promises";
import { join } from "node:path";
/** Hash relative names, bytes and symlink targets in stable lexical order, without following links. */
export async function sourceIntegrity(root: string) {
const tree = new Bun.CryptoHasher("sha256");
let files = 0;
let bytes = 0;
const lfsPointers: string[] = [];
async function visit(prefix: string) {
const entries = await readdir(join(root, prefix), { withFileTypes: true });
entries.sort((a, b) => (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
for (const entry of entries) {
const path = prefix ? `${prefix}/${entry.name}` : entry.name;
if (entry.isSymbolicLink()) {
tree.update(JSON.stringify([path, "symlink", await readlink(join(root, path))]) + "\n");
} else if (entry.isDirectory()) {
tree.update(JSON.stringify([path, "directory"]) + "\n");
await visit(path);
} else if (entry.isFile()) {
const file = Bun.file(join(root, path));
const hash = new Bun.CryptoHasher("sha256");
let count = 0;
for await (const chunk of file.stream()) {
hash.update(chunk);
count += chunk.length;
}
files++;
bytes += count;
tree.update(JSON.stringify([path, "file", count, hash.digest("hex")]) + "\n");
if (count < 1024 && (await file.slice(0, 80).text()).startsWith("version https://git-lfs.github.com/spec/v1"))
lfsPointers.push(path);
} else throw new Error(`Unsupported source entry ${path}`);
}
}
await visit("");
return { files, bytes, lfsPointers, treeSHA256: tree.digest("hex") };
}
+73
View File
@@ -0,0 +1,73 @@
import { sourceIntegrity } from "./source-integrity.ts";
type RuntimeReceipt = {
repository: string;
target: string;
abi: string;
revision: string;
version: string;
sha256: string;
bytes: number;
qualification: { passed: boolean; gates: string[] };
};
export async function verifyPayload(root = Bun.env.BUN_OS_ROOT ?? "/opt/bun-os") {
const receipt = (await Bun.file(`${root}/distribution.json`).json()) as {
name: string;
runtime: RuntimeReceipt;
};
const runtime = receipt.runtime;
if (
runtime?.repository !== "aphrody-labs/bun" ||
runtime.abi !== "gnu" ||
!/^[0-9a-f]{40}$/.test(runtime.revision) ||
!/^[0-9a-f]{64}$/.test(runtime.sha256) ||
!runtime.qualification?.passed ||
!runtime.qualification.gates.includes("native-runtime")
) {
throw new Error("The payload requires a qualified Aphrody Bun runtime receipt");
}
const target = process.arch === "x64" ? "linux-x64" : process.arch === "arm64" ? "linux-arm64" : "";
if (process.platform !== "linux" || runtime.target !== target) {
throw new Error(`Runtime target ${runtime.target} does not match ${process.platform}/${process.arch}`);
}
const child = Bun.spawn(["getconf", "GNU_LIBC_VERSION"], { stdout: "pipe", stderr: "pipe" });
const libc = (await new Response(child.stdout).text()).trim();
if ((await child.exited) !== 0 || !/^glibc \d+\.\d+$/.test(libc))
throw new Error("The image does not provide GNU glibc");
const binary = Bun.file(`${root}/runtime/bun`);
const header = new Uint8Array(await binary.slice(0, 4).arrayBuffer());
if (header.join(",") !== "127,69,76,70" || binary.size !== runtime.bytes) {
throw new Error("Runtime ELF header or byte count differs from the receipt");
}
const actualHash = new Bun.CryptoHasher("sha256").update(await binary.arrayBuffer()).digest("hex");
if (actualHash !== runtime.sha256 || Bun.revision !== runtime.revision || Bun.version !== runtime.version) {
throw new Error("Executing Bun does not match the exact staged runtime");
}
for (const path of [
"sources/bun/package.json",
"sources/material/package.json",
"sources/cosmic-epoch/README.md",
"sources/libcosmic/Cargo.toml",
]) {
if (!(await Bun.file(`${root}/${path}`).exists())) throw new Error(`Missing distribution source ${path}`);
}
const sourceReceipt = await Bun.file(`${root}/sources.receipt.json`).json();
if (sourceReceipt.complete !== true || sourceReceipt.kind !== "bun-os-sources")
throw new Error("Incomplete source receipt");
if (
!sourceReceipt.repositories.some(
(source: { id: string; revision: string }) => source.id === "bun" && source.revision === runtime.revision,
)
)
throw new Error("Source receipt and runtime revision differ");
const contents = await sourceIntegrity(`${root}/sources`);
if (contents.treeSHA256 !== sourceReceipt.treeSHA256)
throw new Error("Image sources differ from their exported content checksum");
return receipt;
}
if (import.meta.main) {
const receipt = await verifyPayload();
console.log(JSON.stringify({ name: receipt.name, revision: receipt.runtime.revision, runtime: "verified" }));
}
+50
View File
@@ -0,0 +1,50 @@
{
"schemaVersion": 1,
"name": "Bun OS",
"version": "1.4.4-dev",
"runtimeKernel": "aphrody-labs/bun",
"hostKernels": ["linux", "windows", "darwin"],
"desktop": "aphrody-labs/cosmic-epoch",
"toolkit": "aphrody-labs/libcosmic",
"material": "aphrody-labs/material",
"branches": { "bun": "os", "material": "os", "libcosmic": "os", "cosmic-epoch": "os" },
"profiles": {
"full": {
"description": "Complete Bun fork and Material sources, COSMIC desktop and applications, GPU renderer",
"renderer": "wgpu",
"helperFeatures": ["gpu"],
"materialFeatures": ["cosmic-wgpu"],
"sources": "complete",
"desktopApplications": "complete"
},
"perf": {
"description": "Same complete source distribution, demand-loaded runtime and software GUI without optional engines",
"renderer": "tiny-skia",
"helperFeatures": [],
"materialFeatures": ["cosmic"],
"sources": "complete",
"desktopApplications": "core"
}
},
"targets": {
"linux-x64": {
"rust": "x86_64-unknown-linux-gnu",
"abi": "gnu",
"libc": "glibc",
"desktop": "cosmic-session",
"container": true
},
"linux-arm64": {
"rust": "aarch64-unknown-linux-gnu",
"abi": "gnu",
"libc": "glibc",
"desktop": "cosmic-session",
"container": true
},
"windows-x64": { "rust": "x86_64-pc-windows-msvc", "abi": "msvc", "libc": "ucrt", "desktop": "winit", "container": false },
"macos-arm64": { "rust": "aarch64-apple-darwin", "abi": "darwin", "libc": "libSystem", "desktop": "winit", "container": false },
"wasm": { "rust": "wasm32-unknown-unknown", "abi": "none", "libc": "none", "desktop": "browser", "container": false }
},
"unresolvedRequirements": ["Identify the owner reference 9937-389B"],
"qualification": "development"
}
+102
View File
@@ -0,0 +1,102 @@
# Standalone application builds
`infra/apps.json` inventories actual apps and their capability owners.
Paths resolve from that catalog. Native CLI apps use their existing Rust
executable; the Bun wrapper embeds qualified backend bytes. Bun CLI/server
apps use their configured entrypoints. An unresolved external context or
missing native executable prevents an independent build.
## Owners
| Capability | Owner |
| --- | --- |
| Compilation and embedded files | Native `Bun.build`, `compile.assets`, `Bun.embeddedFiles` |
| Cloudflare control and credentials | Runtime `packages/bun-cloudflare`; domain changes use the configured MCP |
| R2 object authentication | Canonical `createR2Client` and `loadCloudflareEnvironment` |
| File identity | `src/distribution.ts: identity` |
| Embedded byte identity | Shared `contentIdentity(Blob)` |
| Executable headers | Package converter `abi` and the GNU loader gate in `src/elf.ts` |
| Application orchestration | `infra/app-build.ts` |
| Shared object plans | `infra/assets.ts` |
| Edge and public catalog | The edge/catalog capability owner |
## Configuration
A build uses the selected native runtime binary or an explicit target snapshot.
The snapshot pins its path, SHA256, version and revision. Cross-target snapshots
also require a matching native target receipt with the `native-runtime` gate.
The compiler and snapshot revisions must agree. Runtime metadata reports its
source URL; archive authentication is a separate qualification.
The optional `build` object declares `outputRoot`, `timeoutSeconds`,
`sharedHeavyLock` and `lockKind`. Output paths are relative to the catalog.
The lock kind is `flock` on a qualified Linux host or `exclusive-file`
for the existing native Windows lease protocol. Acquisition is immediate:
an occupied lease is preserved. The workflow also uses the existing
distribution `.factory.lock`.
Each native app provides `native.artifactsByTarget[target]`:
- `path` and `sha256`: exact backend artifact.
- `sourceRevision`: its application source pin.
- `qualificationReceipt`: matching source/artifact evidence with
`native-backend` and `native-dependency-closure` gates.
- `dependencies`: additional embedded native files, each with path and SHA256,
an executable/library role, and any ABI or minimum glibc requirements.
- `systemLibraries`, `abi`, `minimumGlibc`: target requirements retained
from the native owner.
Artifact paths resolve from the catalog. ABI inspection preserves architecture
and runtime distinctions. GNU ELF executables require the actual glibc loader.
The shared ABI owner currently qualifies PE and ELF; Mach-O remains a separate
owner extension.
## Build and release
```sh
bun infra/app-build.ts plan apt --config infra/apps.json --target windows-x64
bun infra/app-build.ts build apt --config infra/apps.json --target windows-x64 --out /NEW/OUTPUT
bun infra/app-build.ts release apt --config infra/apps.json --receipt /NEW/OUTPUT/receipt.json --out /RELEASE.json
```
Compilation bundles JavaScript, rejects unresolved imports, disables build-time
environment inlining and executable dotenv/config autoloading, and embeds
explicit approved assets. The source graph and input checksums accompany the
artifact. Changed runtime or source inputs fail the build.
Native wrappers verify embedded payload hashes and materialize one cache
object per digest. Loader views use hard links, preserving dependency
filenames without copying blobs for each app. Arguments and streams pass
to the existing executable. The native owner retains responsibility for
system libraries, protected stores and application behavior.
A build receipt distinguishes binary production, ABI checks, runtime snapshot,
embedded dependencies, application execution and deployment. A release stage
contains content-addressed references. Edge/catalog publication owns routing
and release activation.
## Shared assets
```sh
bun infra/assets.ts plan --config infra/apps.json --out /ASSET-PLAN.json
bun infra/assets.ts upload --config infra/apps.json --plan /ASSET-PLAN.json --out /ASSET-RECEIPT.json
```
Only explicitly configured `assetRoots` are enumerated. Symlinks, protected
stores, credentials, provider sessions, datasets, game data and model weights
are excluded. Entry count, depth, object size and execution time are bounded.
Assets use `objects/sha256/<digest>` globally. App references contain paths,
content types and object identities; no object payload is copied under an app
prefix. The uploader checks immutable source bytes, uses conditional writes,
and verifies existing or newly written R2 content by SHA256. Public delivery
through the configured custom domain is qualified by the edge owner.
Credentials stay in the existing Cloudflare native store and environment.
Asset plans and release metadata contain no credentials or signed URLs.
Primary API references:
[Bun executables](https://bun.com/docs/bundler/executables),
[Bun S3](https://bun.com/docs/runtime/s3),
[R2 S3 operations](https://developers.cloudflare.com/r2/api/s3/api/).
The configured fork declarations and native CLI help govern available flags.
+49
View File
@@ -0,0 +1,49 @@
# Application domains
The application hostnames in infra/apps.json use the existing aphrody-app
Worker in the aphrody.app zone. This is the single public edge owner. The
main homepage, www redirect, and assets/dist R2 custom domains retain their
existing ownership.
## Hosts
| App | Public hostname | Edge owner |
| --- | --- | --- |
| Filmcraft | https://filmcraft.aphrody.app | aphrody-app |
| Artcraft | https://artcraft.aphrody.app | aphrody-app |
| Photocraft | https://photocraft.aphrody.app | aphrody-app |
| Vectorcraft | https://vectorcraft.aphrody.app | aphrody-app |
| Effectcraft | https://effectcraft.aphrody.app | aphrody-app |
| Lightcraft | https://lightcraft.aphrody.app | aphrody-app |
| PDFcraft | https://pdfcraft.aphrody.app | aphrody-app |
| Wordcraft | https://wordcraft.aphrody.app | aphrody-app |
| Soundcraft | https://soundcraft.aphrody.app | aphrody-app |
| Designcraft | https://designcraft.aphrody.app | aphrody-app |
| CADcraft | https://cadcraft.aphrody.app | aphrody-app |
| Deckcraft | https://deckcraft.aphrody.app | aphrody-app |
| Gridcraft | https://gridcraft.aphrody.app | aphrody-app |
| Figcraft | https://figcraft.aphrody.app | aphrody-app |
| APT | https://apt.aphrody.app | aphrody-app |
| Desktop | https://desktop.aphrody.app | aphrody-app |
## Routing and qualification
Each hostname is an enabled Workers Custom Domain with preview routing disabled.
Cloudflare owns its DNS record and certificate provisioning. Its managed,
proxied DNS record routes to the Worker; it is not a private backend address.
Domain changes use the configured Cloudflare MCP and preserve existing DNS or
Worker ownership when a hostname conflicts.
A valid DNS answer and HTTPS response qualify public edge routing. The current
edge response remains the existing public site. Application executables,
application behavior, native backends and independent deployment require their
own build and deployment receipts. A hostname response does not qualify those
application capabilities.
The ignored out/app-domains/receipt.json records preflight DNS/domain ownership,
creation results, read-back checks, public DNS/HTTPS observations, and separate
certificate provisioning state. No Worker code upload is part of hostname
attachment. Shared assets and artifacts use their existing R2 custom domains;
application hosts do not duplicate asset objects.
API owner: [Workers Custom Domains](https://developers.cloudflare.com/workers/configuration/routing/custom-domains/).
+105
View File
@@ -0,0 +1,105 @@
{
"schemaVersion": 1,
"observedAt": "2026-10-13T11:52:53.876Z",
"materialRevision": "1e65a503420e4a8962b8ceaa18819eedbc280454",
"apt": {
"schemaVersion": 1,
"kind": "apt-consolidation-qualification",
"observedAt": "2026-10-13T11:50:48.306Z",
"artifact": {
"path": "apt.exe",
"bytes": 171469824,
"sha256": "43cacda8663021aa0640297978ef600b0d43bf8fe715d04a0f5464ee96bb2fbd"
},
"materialSource": "1e65a503420e4a8962b8ceaa18819eedbc280454",
"runtime": {
"version": "1.4.4",
"revision": "2ae8fc65fba1d05c423a2433b1195835109942b6",
"sha256": "a6f79203a9601914e86479f103c7f695330d54442169566f64e29328440d9c89",
"stableRelease": false,
"lockedSourceRuntime": false
},
"ubuntu": {
"dsc": {
"bytes": 3127,
"sha256": "364347278a531cf073f809a14f84617c2b935cfb324bd413aa01c214b6f8a3e8"
},
"archive": {
"bytes": 2476660,
"sha256": "dcd186912a7b8553cc885412127925858ee340c655e2f26e77eacdab23f48089"
},
"payloadCount": 926,
"archiveAuthenticationVerified": true,
"developerSignatureVerified": false
},
"msys2": {
"archive": {
"bytes": 111475,
"sha256": "841401182976d2f9e17e5c0ebaac51f2a8014140ea53d67625e91c8fb3c85ea0"
},
"publisherChecksumVerified": true,
"detachedSignatureVerified": false,
"dll": {
"bytes": 127291,
"sha256": "11cd91171765cd65f1c3362e3df67e5f5a618a6b88c8cdec899b802eaf1c7b0d"
},
"payloadBytesPreserved": true,
"dllExecution": false
},
"winget": {
"manifest": {
"bytes": 648,
"sha256": "f20b7e5ee9703639db222254197fd07c805b3707f051590dc7b89b160f40596b"
},
"manifestValidated": true,
"publicDownloadVerified": true
},
"qualification": {
"aptVersion": true,
"sourceConversion": true,
"nativeLibraryConversion": true,
"nativeLibaptEngine": false,
"wholeAppBehavior": false,
"testsRun": false
},
"publicDownloadVerified": true,
"download": "https://dist.aphrody.app/objects/sha256/43cacda8663021aa0640297978ef600b0d43bf8fe715d04a0f5464ee96bb2fbd",
"publication": {
"schemaVersion": 1,
"kind": "apt-public-release",
"bytes": 171469824,
"sha256": "43cacda8663021aa0640297978ef600b0d43bf8fe715d04a0f5464ee96bb2fbd",
"objectKey": "objects/sha256/43cacda8663021aa0640297978ef600b0d43bf8fe715d04a0f5464ee96bb2fbd",
"created": true,
"checksumVerified": true,
"url": "https://dist.aphrody.app/objects/sha256/43cacda8663021aa0640297978ef600b0d43bf8fe715d04a0f5464ee96bb2fbd",
"status": 200,
"publicDelivery": true,
"publicChecksumVerified": true,
"cacheControl": "public, max-age=31536000, immutable",
"nativeLibaptEngine": false,
"wholeAppBehavior": false
}
},
"wasm": {
"sourceRevision": "ce47e50324ce4053c4fcd08be59db55254e231f7",
"sha256": "c1abee38ea03c33b8150c14ef987906759182a6fd693c636b960922962c69ed8",
"bytes": 113528,
"abi": 2,
"url": "https://assets.aphrody.app/objects/sha256/c1abee38ea03c33b8150c14ef987906759182a6fd693c636b960922962c69ed8",
"publicChecksumVerified": true,
"completeDesktop": false
},
"domains": {
"receiptSHA256": "8679624addfded2b9fbc5cd44b917bf5c4f72ca910332ad3509b52c65b021e6d",
"hosts": 16,
"routingQualified": true,
"nativeAppsDeployed": false
},
"limits": [
"APT native libapt engine is not ported.",
"XZ and bzip2 conversions require an external native 7-Zip decoder.",
"The selected Bun runtime differs from the locked Bun source revision.",
"WASM is the scalar Material token contract from ce47e503, not Bun/JSC or a complete desktop."
]
}
+31
View File
@@ -0,0 +1,31 @@
# Upstream conventions
Use Linux kernel, Debian, COSMIC and Bun upstream conventions.
Rust uses default rustfmt, four spaces, Rust naming,
rustdoc documentation and explicit errors. Document unsafe contracts and
exported symbols. Window event loops, system calls and application resources
remain with their owners.
TypeScript follows Bun upstream: two spaces, width 120, semicolons, trailing
commas and unparenthesized single arrow parameters. Use the APIs available
in the actual installed runtime; consult its declarations before adding flags.
COSMIC follows Application/Core/Message models and Element composition.
Preserve focus, state, inherited colors, compact navigation and pinned API limits.
Debian packaging uses explicit files and dependencies, extension-free installed
commands, separate configuration and artifacts, short symlinks, minimal permissions
and checked command status.
Use Bun-native APIs and commands. Run aggressive `bun n2b` across the OS
workspace and review findings that require a manual decision. Run `bun check`,
`bun lint` and `bun fmt` directly. Confirm technical choices in primary sources.
References:
- [Linux Rust guidelines](https://docs.kernel.org/rust/coding-guidelines.html).
- [Debian Policy: files](https://www.debian.org/doc/debian-policy/ch-files.html).
- [Bun upstream formatter](https://github.com/oven-sh/bun/blob/main/.prettierrc).
- [COSMIC application template](https://github.com/pop-os/cosmic-app-template).
- [libcosmic API](https://pop-os.github.io/libcosmic/cosmic/).
+187
View File
@@ -0,0 +1,187 @@
# Deployment control
`infra/apps.json` is the single desired-state catalog. It owns app identities,
hosts, source ownership, native requirements, releases and shared R2 references.
`infra/control-plane.json` supplies pinned control components and their
qualification contracts. `infra/compose.yaml` is a bootstrap adapter for those
components. There is no second app catalog, source authority or deployment
orchestrator here.
Komodo Core/Periphery is the Rust deployment executor. Forgejo and its runner
own Git hosting and workflows. Existing native Bun owners build apps, export
pinned sources and upload content-addressed assets. Cloudflare MCP and the
existing edge adapter own routes. Existing GitHub forks keep their histories
and ownership; no automatic repository/provider-auth synchronization is added.
## Verified component pins
The component profile is authoritative for complete `image:tag@sha256` refs.
Its six OCI index digests were checked against registry response headers and
SHA-256 of the returned manifest bytes on 13 October 2026. Image layers were
not pulled, and no service was started.
| Component | Pinned release | Role |
| --- | --- | --- |
| Komodo Core/Periphery | 2.3.3 | Rust deployment control and agent |
| FerretDB | 2.7.0 | Mongo protocol for the Komodo database client |
| PostgreSQL DocumentDB | 17-0.107.0-ferretdb-2.7.0 | Matched PostgreSQL 17.6 backend |
| Forgejo rootless | 15.0.10 LTS | Git forge, SQLite, Actions UI |
| Forgejo Runner | 13.2.0 | Isolated workflow execution |
All selected images publish Linux amd64 and arm64 manifests. These published
platforms are not native execution qualifications. Forgejo's LTS is supported
through 15 July 2027. The short stable 16.0 branch ends support on 29 October
2026, so this proposal uses the current LTS. [Forgejo releases](https://forgejo.org/releases/)
Komodo's release resolves to source commit
`780ac68b992094a9fccd5fffb760e0c84fd3c3d1`.
[Komodo 2.3.3 release](https://github.com/moghtech/komodo/releases/tag/v2.3.3)
The selected database pair is explicitly matched by the upstream releases,
and Komodo provides this backend in its pinned compose.
[FerretDB 2.7.0](https://github.com/FerretDB/FerretDB/releases/tag/v2.7.0),
[DocumentDB pair](https://github.com/FerretDB/documentdb/releases/tag/v0.107.0-ferretdb-2.7.0),
[Komodo FerretDB compose](https://github.com/moghtech/komodo/blob/v2.3.3/compose/ferretdb.compose.yaml)
## Host and activation boundary
A read-only `bun ssh` probe at `2026-10-13T10:13:45.389Z` observed the VPS as
x64 with AVX, kernel `7.0.0-38-generic`, 127,031,021,568 available build-disk
bytes and 17,583,894 free build-disk inodes. This snapshot does not qualify
the future persistent deployment daemon or reserve capacity against other jobs.
That kernel is within MongoDB's unsupported 6.19 through 7.0.13 range.
MongoDB refuses startup there, including inside Docker. The selected FerretDB
backend avoids that MongoDB server binary; it still needs its own runtime
qualification. No kernel change, reboot or database migration is proposed.
[MongoDB kernel contract](https://www.mongodb.com/docs/manual/release-notes/8.0/)
Do not activate while the OS factory has active heavy jobs. Inspect their
terminal receipts and the shared heavy lock. Qualify a separate persistent
deployment Docker context/service with its data root on the configured
build-disk `~/apps` area. Its socket becomes `DEPLOYMENT_DOCKER_SOCKET`.
CI uses another persistent qualified socket, `CI_DOCKER_SOCKET`, and Docker
25.0 or later. The factory's isolated ephemeral engine and the global Docker
daemon retain their owners and workloads. Neither is an implicit fallback.
[Runner Docker requirement](https://forgejo.org/2026-08-runner-release-v13/)
Every app release and route remains in `apps.json`. Resolve `deploy`, `git` and
`ci` roles through that catalog's edge owner. Core HTTP, Forgejo HTTP and Git
SSH are published only on loopback. Cloudflare HTTPS requires a separately
qualified origin/tunnel route; DNS alone cannot reach a loopback listener.
The runner has no public HTTP service. A `ci` route must use Forgejo's Actions
UI through the edge owner. Git SSH needs its actual qualified SSH access host
and port; ordinary Cloudflare HTTP proxying does not provide SSH transport.
## Persistence and credentials
All bind roots are explicit and must exist before startup; Compose does not
create them. Resolve `DEPLOYMENT_STATE_ROOT` to a fresh, owned persistent
subdirectory of the qualified build-disk apps root. Keep managed app checkouts
in the separate `DEPLOYMENT_MANAGED_ROOT`. Periphery must see that managed root
at the same absolute path inside and outside its container.
[Periphery contract](https://github.com/moghtech/komodo/blob/v2.3.3/config/periphery.config.toml)
| State directory | Expected owner | Container path |
| --- | --- | --- |
| `postgres` | 999:999 | `/var/lib/postgresql/data` |
| `ferretdb` | 1000:1000 | `/state` |
| `keys` | Komodo root process | `/config/keys` |
| `forgejo` | 1000:1000 | `/var/lib/gitea` |
| `runner` | 1000:1000 | `/data` |
The profile records source/configuration evidence for numeric owners; verify
actual image users and mounted-file access when qualifying the layers.
[FerretDB user](https://github.com/FerretDB/FerretDB/blob/v2.7.0/build/ferretdb/passwd),
[PostgreSQL base source](https://github.com/docker-library/postgres/blob/87e6f65859a53d10c5170a587def1bfc882d3830/17/bookworm/Dockerfile),
[Forgejo rootless image](https://codeberg.org/forgejo/forgejo/src/tag/v15.0.10/Dockerfile.rootless)
The credential owner supplies the profile's `*_FILE` inputs and the isolated
`DEPLOYMENT_SECRETS_ROOT`. No credential values belong in the catalog, tracked
env files, command arguments or output. File-backed Compose secrets retain
their host access permissions; grant the intended container UID access to the
specific file. Database password files serve the initial database/Core login;
the FerretDB PostgreSQL URI is a separate private file with percent-encoded
credentials and address `database:5432/postgres`. FerretDB supports native
`FERRETDB_POSTGRESQL_URL_FILE` and SCRAM-SHA-256 authentication.
[FerretDB flags](https://github.com/FerretDB/FerretDB/blob/v2.7.0/website/versioned_docs/version-v2.7/configuration/flags.md),
[authentication](https://github.com/FerretDB/FerretDB/blob/v2.7.0/website/versioned_docs/version-v2.7/security/authentication.md)
Komodo uses native database, bootstrap-password, JWT and webhook secret-file
inputs. Its generated Core/Periphery communication keys share one persistent
keys directory. The outbound agent connects to `ws://core:9120`; no 8120 port
is published. Its Docker socket permits administration of the chosen deployment
daemon. Default global-update and dated-backup procedures are disabled through
`KOMODO_DISABLE_INIT_RESOURCES=true`.
[Komodo configuration](https://github.com/moghtech/komodo/blob/v2.3.3/config/core.config.toml),
[key/outbound settings](https://github.com/moghtech/komodo/blob/v2.3.3/compose/compose.env)
Forgejo rootless stores SQLite, repositories, configuration and generated keys
under `/var/lib/gitea`. Use a real bootstrap admin account before opening its
origin route. Installation is locked and public registration is disabled.
Changing initialization secret files does not rotate credentials in an existing
database. Existing Mongo/FerretDB v1 storage cannot be reused as this backend.
No protected database or latest off-host backup is moved or deleted here.
[Komodo migration contract](https://github.com/moghtech/komodo/blob/v2.3.3/bin/cli/docs/copy-database.md)
## Thin adapter contract
The existing Bun deployment owner reads the profile alongside `apps.json`:
1. Map each `images.*.environment` to that record's immutable `reference`.
Map the profile's loopback ports and shared server name to their Compose
bindings; these defaults have one owner in the component profile.
2. Resolve actual route URLs, deployment context, native platform, managed/data
roots, bootstrap owner and isolated secret-file paths from qualified owner
configuration. Never use the factory/global daemon as a missing-value default.
3. Apply the bootstrap Compose through the persistent deployment context once
its activation gates pass. Periphery executes subsequent app deployments
from the same catalog; do not introduce a second SSH/Docker app deploy loop.
4. Register a runner through the Forgejo UI/API. Keep its actual UUID and token
with the registration owner. Resolve the profile's `runner.configuration`
bindings and write the resulting YAML with native `Bun.YAML.stringify` to
`FORGEJO_RUNNER_CONFIG_FILE`. The token remains a separate secret file.
5. Enable the `ci` profile only after validating its exact socket path/GID,
token, UUID, public forge URL, configuration file and immutable Bun job image.
The base stack does not need a fictitious pre-registration runner credential.
The inactive runner's default file/socket locations are future paths in the
explicit owned state/secret roots. They are not readiness or registration
proof. The CI adapter must require the real values before activation.
The canonical runner configuration sets capacity 1, a 45-minute job bound,
no job privilege or Docker socket, no allowed host volumes, and disabled cache.
It uses `server.connections` and a file token, rather than deprecated GITEA
variables/registration. Its Docker endpoint is separate from the deployment
daemon. [Runner 13.2 configuration](https://code.forgejo.org/forgejo/runner/src/tag/v13.2.0/internal/pkg/config/config.example.yaml),
[Forgejo LTS registration](https://forgejo.org/docs/v15.0/admin/actions/registration/)
Jobs invoke the existing Bun app/native factory owners. An immutable job image
must carry the exact qualified Aphrody Bun runtime and required native tools;
no stock Bun downloader or Node dependency is introduced. JavaScript Actions
commonly require Node and are not automatically compatible with these Bun
shell workflows. Runner presence does not qualify each app's native backend,
standalone binary, ABI, release or shared R2 objects.
## Readiness and qualification
PostgreSQL's `pg_isready` gates FerretDB startup. The image's native
`/ferretdb ping` then checks Mongo-protocol connectivity, PostgreSQL access and
DocumentDB installation. Core's `/version` endpoint begins serving after its
startup/database initialization; it is not a continuous database probe.
Periphery is qualified through Core's authenticated connected-server health,
not an invented inbound endpoint.
[FerretDB readiness](https://github.com/FerretDB/FerretDB/blob/v2.7.0/website/versioned_docs/version-v2.7/configuration/observability.md),
[Core router](https://github.com/moghtech/komodo/blob/v2.3.3/bin/core/src/api/mod.rs)
Forgejo's `/api/healthz` checks its database/cache when installation is locked.
Before installation it can return success without those checks, so bootstrap,
authenticated Git/API use and one bounded Bun workflow are separate gates.
Workflow logs/artifacts are retained for seven days; Docker logs are bounded
to one 10 MB file per service. These generated outputs do not authorize deletion
of raw provider sessions, protected databases, weights or backups.
[Forgejo health](https://codeberg.org/forgejo/forgejo/src/tag/v15.0.10/routers/web/healthcheck/check.go)
Current evidence qualifies upstream metadata and the local manifest syntax.
It does not qualify image layers, service startup, secrets, runner registration,
app execution, HTTPS routes or production deployment. No layer pull, deployment,
test, database migration, WSL start, commit or push was performed by this batch.
+63
View File
@@ -0,0 +1,63 @@
# Native source targets
`factory/native-targets.ts` builds the source owners pinned by `sources.lock.json`.
Its catalog separates executable build steps, host prerequisites and runtime
qualification. Source checkouts must match the exact pin and contain no concurrent
changes. Output directories are new, outside the sources, and inside the selected
factory. Jobs take the factory's exclusive lock and require 50 GiB free. Linux
builds also hold the existing shared heavy-job lock from `factory/profile.json`;
its parent directory must be provisioned by the factory owner.
```sh
bun factory/native-targets.ts plan apt-gnu
bun factory/native-targets.ts status apt-gnu --source ../apt --out /home/build/apt-job
bun factory/native-targets.ts build apt-gnu --source ../apt --out /home/build/apt-job
```
| Target | Build host | Owner artifact |
| ----------------- | ------------------------------------- | ---------------------------------------------------------- |
| linux-aphrody-bun | Native Linux x64 | Kernel, modules and headers from the aphrody-bun branch |
| wsl-windows | Native Windows x64, VS2022/UCRT | WSL PE executables, using native Clang for guest helpers |
| wslg-system | Linux x64 Docker factory | WSLg system tar and VHD from its Azure Linux Dockerfile |
| wsl-kernel | Native Linux x64 | Aphrody WSL defconfig kernel and modules/headers/perf VHDX |
| apt-gnu | Native Linux x64/glibc | Official APT libraries, tools and methods |
| apt-windows-msvc | Proposed native Windows x64/UCRT port | libapt-pkg and apt.exe after the APT source port |
For `linux-aphrody-bun`, pass an explicit `--kernel-config` selecting x86_64 and
`CONFIG_BUN_ACCEL` and `CONFIG_RUST`. The WSL kernel uses its branch's
`arch/x86/configs/aphrody_wsl_defconfig`, which selects built-in Bun acceleration.
Both kernel targets use `LLVM=1`, Rust, matching `rust-src`, libclang and bindgen;
the owner's `rustavailable` target validates compiler compatibility before the
kernel build. Builds recheck those selections after `olddefconfig` and use
an output `.config`; the input configuration is not overwritten. WSL native builds select Visual Studio 2022 and SDK
10.0.26100.0, and reject `UserConfig.cmake` to avoid automatic deployment.
These recipes never register distributions, edit `.wslconfig`, or start WSL.
WSLg requires `--config` with an explicit version, immutable `baseImage` digest,
and `externalPins` mapping each vendor path in the catalog to its exact Git SHA.
FreeRDP, Weston, PulseAudio, Mesa and DirectX-Headers are external sources rather
than gitlinks. Their checkouts must already exist at the required `vendor/` paths.
The Dockerfile's component licenses remain with the artifacts. The Windows RDP
plugin has a separate native Visual Studio build owner.
The official APT pin requires C++23, GNU/POSIX file/process/signal interfaces,
Unix generators and libraries. Its native Windows recipe fails before a build
until those source owners are ported. The practical port boundary includes
MSVC DLL exports and generators, UCRT 64-bit file offsets, Win32 mapping/locking,
Unicode paths, process/pipes, dependencies built for the same runtime, signature
verification, and an explicit package transaction backend. The Bun package
conversion frontend named `apt.exe` is a distinct artifact.
GNU APT installation uses a job-local `DESTDIR`, preserving `/usr` runtime paths
without installing files into the factory host's package directories.
Build receipts contain artifact hashes and ABI evidence. Boot, deployment,
signature trust, package transactions and desktop/GPU behavior remain separate
qualification steps.
Pinned source references:
- [Linux fork](https://github.com/aphrody-labs/linux/tree/47f637c546bf4bb274c7149815ed66d748e4f325).
- [WSL native build](https://github.com/aphrody-labs/WSL/blob/341f1ddf76f089ff6351590b03f92bad30041725/doc/docs/dev-loop.md).
- [WSLg build sources](https://github.com/aphrody-labs/wslg/blob/6942bb534b537302a7a22617261d9274db3cfa37/CONTRIBUTING.md).
- [WSL kernel owner](https://github.com/aphrody-labs/WSL2-Linux-Kernel/blob/e5043002ed6f0b0eb17a0fb9efafa794045853ed/README.md).
- [Official APT](https://salsa.debian.org/apt-team/apt/-/tree/745fc126218c7cb3df48b22bbc7c7098fa2f7966).
+117
View File
@@ -0,0 +1,117 @@
# Rust systems catalog
Primary-source review: **13 October 2026**. This catalog records package identities,
feature boundaries and potential uses. It adds no dependencies or release pins.
Version-specific links identify the source inspected; integration must use the
capability owner's qualified lockfile and target configuration.
## Architecture fit
The distribution targets the owned Linux kernel and a GNU/glibc userspace
with Bun and COSMIC. Native Windows keeps its Windows/UCRT execution
context. Bun owns runtime and host interfaces; Material owns M3; COSMIC owns its
window/event model. The factory packages those owners. See
[native targets](NATIVE-TARGETS.md) and [upstream conventions](CODING.md).
| Area | Current integration boundary | Future bare-metal boundary |
| --- | --- | --- |
| Boot, CPU, MMIO and drivers | Existing Linux/Windows kernel and driver owners; native factory tools can produce boot artifacts. | A separately qualified firmware/kernel target owns privilege levels, address spaces, interrupts, DMA and hardware discovery. |
| Heaps and synchronization | Reuse Bun/JSC and existing Rust host allocation/threading contracts. | A kernel owner supplies mapped memory, allocator initialization and interrupt-safe locking. |
| WASM | Reuse Bun's existing WebAssembly host; a Rust sandbox can be an optional extension behind the same capability owner. | Interpreter/compiler support also needs allocation and explicit host imports. |
| Graphics | Reuse Material's renderer and the pinned COSMIC backend; backend features belong there. | Hardware/display drivers and graphics API support need independent qualification. |
| Network, formats and CLI | Extend existing Bun/package-conversion/host owners through their APIs. | A device backend, protocol stack and execution environment must be implemented explicitly. |
UEFI or a Rust bootloader does not provide the libc, threads, virtual memory,
signals and platform services required by the present Bun/JSC runtime. A firmware
image therefore does not qualify Bun or COSMIC on bare metal. This is an
architecture inference from the current [native target contract](NATIVE-TARGETS.md).
## Boot, CPU and low-level access
| Canonical package | Verified contract | Placement |
| --- | --- | --- |
| [uefi](https://docs.rs/crate/uefi/0.41.0/source/src/lib.rs) | no_std firmware interfaces; optional allocation, global allocator and logging helpers. Its Boot Services allocator cannot be used after exit_boot_services. | Firmware utilities and a future boot owner. Keep post-exit memory ownership separate. |
| [bootloader](https://docs.rs/crate/bootloader/0.11.17/source/src/lib.rs) | The published library is a std host-side image builder, with BIOS/UEFI features. It produces x86_64 boot images. | An optional factory tool for experimental boot artifacts; its builder crate is not a no_std kernel library. |
| [x86_64](https://docs.rs/crate/x86_64/0.15.5/source/src/lib.rs) | no_std architecture types/instructions; observed defaults include nightly and instructions. Stable use selects supported features with defaults disabled. | Address types may support tools; privileged instructions remain with the boot/kernel owner. |
| [riscv](https://docs.rs/crate/riscv/0.16.1/source/src/lib.rs) | no_std register/architecture interfaces. critical-section-single-hart implements a single-hart interrupt policy and is unsound for a multi-hart system. | Future RISC-V firmware/kernel work with an explicit hart and interrupt model. |
| [aarch64-cpu](https://docs.rs/crate/aarch64-cpu/11.2.0/source/src/lib.rs) | no_std CPU registers/instructions; access depends on the current exception level. | Future AArch64 firmware/kernel work; normal host applications use OS APIs. |
| [volatile](https://docs.rs/crate/volatile/0.6.1/source/src/lib.rs) | no_std volatile raw-pointer wrappers; optional derive/unstable APIs. Volatile accesses do not supply synchronization or valid mappings. | MMIO through an owner-provided mapping and access contract. |
| [spin](https://docs.rs/crate/spin/0.12.3/source/src/lib.rs) | no_std spin synchronization; optional std yielding and alternative mutex policies. | Low-level contexts that own interrupt/reentrancy rules; ordinary host waits retain their host threading owner. |
## Allocation
| Canonical package | Verified contract | Placement |
| --- | --- | --- |
| [linked_list_allocator](https://docs.rs/crate/linked_list_allocator/0.10.6/source/src/lib.rs) | no_std free-list heap. Default use_spin uses spinning_top and exposes LockedHeap; alloc_ref enables nightly allocator APIs. | A caller-owned heap or future kernel allocator, with an explicitly supplied memory region. |
| [talc](https://docs.rs/crate/talc/5.1.1/source/README.md) | no_std allocator with a memory-source abstraction. TalcLock<R, S> requires the caller's lock_api::RawMutex implementation; TalcCell is single-threaded. There is no bundled default locking feature in the inspected release. | An optional arena/WASM/kernel heap after memory-source, synchronization and growth policy are defined. |
| [buddy_system_allocator](https://docs.rs/crate/buddy_system_allocator/0.13.0/source/src/lib.rs) | no_std buddy allocator; observed defaults are alloc plus use_spin. Frame-allocation and LockedHeap/global-allocator APIs are feature-gated. | Primarily future page/heap management. It does not allocate physical memory from the host by itself. |
The names linked-list-allocator and buddy-allocator are catalog labels; the
verified Cargo identities are linked_list_allocator and buddy_system_allocator.
TalcLock uses a supplied lock implementation; its synchronization properties
depend on that implementation. TalcCell serves single-threaded use. These
contracts are explicit in its [current API](https://docs.rs/talc/5.1.1/talc/).
## Devices and buses
| Canonical package or unresolved name | Verified contract | Placement |
| --- | --- | --- |
| [virtio-drivers](https://docs.rs/crate/virtio-drivers/0.13.0/source/src/lib.rs) | no_std VirtIO drivers; alloc/embedded-io defaults. The caller implements the unsafe Hal DMA/MMIO contract. | Future VirtIO guest drivers. Current native applications use the existing host driver interfaces. |
| [pci_types](https://docs.rs/crate/pci_types/0.10.1/source/src/lib.rs) | no_std configuration-space types; access is provided by ConfigRegionAccess. | A candidate for shared PCI types and a qualified enumeration backend. |
| [pci](https://docs.rs/crate/pci/0.0.1/source/src/lib.rs) | A separate legacy x86 port-I/O scanning crate; its source includes old nightly syntax. It does not provide PCIe ECAM support. | Compiler/target compatibility is unqualified; it is not the default modern PCI candidate. |
| pcie-enumeration | The exact [registry query](https://crates.io/api/v1/crates/pcie-enumeration) returned 404. No canonical package was established under that spelling. | Keep this requested identifier unresolved. |
| [pcie](https://docs.rs/crate/pcie/0.7.1/source/src/lib.rs) | A separately verified rcore-os/tgoskits crate: no_std plus alloc, MMIO interfaces and controller enumeration. | A future PCIe candidate requiring its mapping/allocator contracts. It is not an automatic alias for pcie-enumeration. |
| [embedded-hal](https://docs.rs/crate/embedded-hal/1.0.0/source/src/lib.rs) | no_std delay/GPIO/I2C/SPI/PWM traits without an alloc requirement. | Shared driver interfaces implemented by an explicit platform adapter. |
## Optional WASM execution
| Canonical package | Verified contract | Placement |
| --- | --- | --- |
| [wasmtime](https://docs.rs/wasmtime/49.0.2/wasmtime/) | A configurable runtime/compiler with optional Cranelift, component-model, async, threads and runtime features. Defaults enable a broad host runtime. [Minimal embeddings](https://docs.wasmtime.dev/examples-minimal.html) select their feature and execution-engine closure. | Optional native component/WASI hosting when the existing runtime owner needs that contract. Bare-metal/custom-host use needs its own integration; default host features do not establish it. |
| [wasmi](https://docs.rs/wasmi/2.0.0/wasmi/) | A WebAssembly interpreter that supports no_std **with alloc**. Defaults enable std and WAT; disabling std requires disabling defaults and selecting the release's required execution/validation features. The [source](https://github.com/wasmi-labs/wasmi/blob/main/crates/wasmi/src/lib.rs) explicitly uses alloc. | An optional constrained Rust interpreter host. Allocation, fuel/memory limits and allowed host imports remain explicit. no_std does not mean no heap. |
| [wasmedge-sdk](https://docs.rs/crate/wasmedge-sdk/0.14.0/features) | Rust bindings to the native WasmEdge C API, backed by wasmedge-sys. Linking/downloading features and native-engine compatibility depend on the chosen SDK release. | Optional native WasmEdge integration through one owner and an exact engine/SDK/target contract. It is not a self-contained pure-Rust bare-metal interpreter. |
WasmEdge is the project name; wasmedge-sdk is the Cargo SDK identity. Its
[upstream main README](https://github.com/WasmEdge/wasmedge-rust-sdk) documents
newer examples/features than the registry release inspected here. Resolve the
SDK/sys/native-engine release matrix before selecting features; this catalog
does not turn the main-branch examples into package pins.
## Graphics and window handles
| Canonical package | Verified contract | Placement |
| --- | --- | --- |
| [wgpu](https://docs.rs/wgpu/30.0.1/wgpu/) | Portable graphics API with selectable native and browser backends. Device, surface, driver and target support are separate requirements. | The existing renderer/COSMIC graphics owner selects its backend closure. A WASM Rust compile is not a GPU/browser rendering result. |
| [wgpu-core](https://docs.rs/wgpu-core/30.0.1/wgpu_core/) | wgpu's implementation/validation core; it belongs to the same release family as the public API and backend crates. | Lower-level integration behind that graphics owner when required. It is not a second windowing or OS framework. |
| [raw-window-handle](https://docs.rs/raw-window-handle/0.6.2/raw_window_handle/) | Interoperable platform window/display handles, with borrowing and validity contracts. It does not create windows or run their event loop. | Bridge the existing COSMIC/native window owner to a compatible renderer; preserve handle lifetime and thread ownership. |
## Networking and binary formats
| Canonical package | Verified contract | Placement |
| --- | --- | --- |
| [smoltcp](https://docs.rs/smoltcp/0.14.0/smoltcp/) | Configurable TCP/IP stack for embedded/no_std operation. Defaults enable std; raw-socket/TUN-TAP device features require std. | Optional userspace/embedded stack with an owned device, timing and socket-resource model. Existing Linux/Windows/Bun networking keeps the native host stack. |
| [etherparse](https://docs.rs/etherparse/0.21.0/etherparse/) | Packet parsing/building; default std can be disabled, with alloc separate. | Extend an existing packet-format owner. Parsing a packet does not implement a network driver or full stack. |
| [zerocopy](https://docs.rs/zerocopy/0.8.62/zerocopy/) | no_std by default; optional alloc/std/derive. Typed byte conversions depend on layout, alignment, validity and lifetime traits. | Shared binary/ABI structures where those contracts can be proven. Preserve endian and input-bound checks. |
| [binrw](https://docs.rs/binrw/0.15.2/binrw/) | Declarative binary read/write derives; defaults include std, while no_std is supported with the matching IO/allocation configuration. | An extension to an existing native binary-format owner; retain bounds and actual payload ABI. |
| [nom](https://docs.rs/nom/8.0.0/nom/) | Byte-oriented parser combinators; defaults enable std, which implies alloc; constrained use selects the appropriate feature closure. | Reuse a parser owner for streaming/incremental formats and explicit incomplete-input handling. |
## Shell and command-line tools
| Verified project/package identity | Contract | Placement |
| --- | --- | --- |
| [uutils/coreutils](https://github.com/uutils/coreutils) / [coreutils](https://crates.io/crates/coreutils) / [uucore](https://crates.io/crates/uucore) | coreutils supplies utility programs; uucore supplies their shared Rust plumbing. Utility and platform features are explicit. The exact uutils-coreutils registry name returned 404. | Native userspace tools or existing CLI adapters. uucore alone is not the complete command suite. |
| [Nushell](https://github.com/nushell/nushell) / [nu](https://crates.io/crates/nu) | Structured-data shell with separate parser, engine, protocol and command crates; features select plugin/network/SQLite/etc. The unrelated nushell registry entry is not the canonical upstream package. | Reuse the existing shell owner through its CLI/crate boundary. It requires a qualified native host and is not a firmware shell by declaration. |
| [clap](https://docs.rs/clap/4.6.7/clap/) | Rust command-line parser; default features include std/help/usage/error context, with derive optional. | Existing Rust CLI entrypoints. Bun application CLIs continue through the Bun owner. |
## Selection record
Choose a package only when an existing owner needs the stated contract. Record
its canonical package/library identity, exact source or registry checksum,
license, target ABI, features and transitive owner closure in that owner's
manifest/receipt. no_std, alloc, allocator locking, privileged access and native
engine requirements are separate properties. Compilation, execution, display,
boot and deployment evidence remain separate.
The supplied external reference App-69ef18c674308191a2f952431f91ea61 has no
resolved capability or primary-source URL in this review's exposed tool inventory. It is
not assigned a crate, repository or deployment role by this catalog.
+341
View File
@@ -0,0 +1,341 @@
# Session report: 13 October 2026
## Scope and result
This session prepared the Bun OS distribution, corrected Material ownership,
established an authenticated Ubuntu build factory, built a Windows package
conversion frontend, and attached the application's public hostnames.
The complete OS objective is not achieved.
Native Windows owns workstation development. The VPS factory is a separate
SSH/Linux execution context. No workstation WSL was started. Source owners,
runtime bytes, packages, desktop, rootfs, images, WASM, performance and public
routing are qualified separately below.
The latest owner request authorizes consolidation/reporting and normal
publication of our owned batches. This report does not revive unrelated
earlier convergence work. Final consolidation repeated the scoped compile
checks and rebuilt/executed apt.exe. No new behavior tests were authored or run.
## Ownership and source graph
Distribution assembly lives in this checkout. Bun remains the canonical
runtime fork on main. Material owns the existing native token/control engine,
libm3 and WASM adapter; it retains its web package submodules. Linux, WSL,
WSLg, the WSL kernel, COSMIC and official APT remain distinct sources with
their own histories and licenses.
The final source lock contains **83 mounts, 77 repository/revision pairs and
74 gitlinks across nine roots**. The original four-root audit had 78 mounts
and 71 pairs. Different revisions of libcosmic/iced/icons remain separate
mounts; deduplication does not collapse version-specific topology.
| Root | Locked revision |
| --- | --- |
| Bun | c209b95eb0dcc964745f4684f127abb83cbe2697 |
| Material/os | 1e65a503420e4a8962b8ceaa18819eedbc280454 |
| COSMIC epoch | cfa498958dc8aeac73aa911cbd318c01e47ac957 |
| libcosmic/os | 7f715bc5d2d383a070fd8c760aa52c5627df96b6 |
| Linux aphrody-bun | 47f637c546bf4bb274c7149815ed66d748e4f325 |
| WSL master | 341f1ddf76f089ff6351590b03f92bad30041725 |
| WSLg main | 6942bb534b537302a7a22617261d9274db3cfa37 |
| WSL kernel aphrody-wsl-6.18 | e5043002ed6f0b0eb17a0fb9efafa794045853ed |
| APT main | 745fc126218c7cb3df48b22bbc7c7098fa2f7966 |
The APT GitHub mirror used initially was archived. The owner fork was then
fast-forwarded from official live Salsa main, normally pushed and checked by
remote ref. That Git source is distinct from Ubuntu's authenticated apt 3.2.0
source package.
Source export now has one owner, exportPinnedSources, reused by the existing
assembler. It implements committed Git/tree/blob checks, recursive gitlinks,
archive bounds, LFS SHA256/size verification, protected-root guards, fresh
outputs, shared locking, and measured byte/inode capacity requirements.
No complete source acquisition/LFS/export receipt exists yet. Large icons
trees need measured capacity and a complete inventory, not a truncated API tree.
Automatic approval review rejected deletion of the transient icons metadata
with "blocked by policy". No deletion or bypass occurred; that ignored
metadata store remains in place.
References: [source lock](../sources.lock.json),
[source export](SOURCE-EXPORT.md), [native targets](NATIVE-TARGETS.md).
## Material correction sequence
1. The initial OS-local libm3 and 2061-byte scalar ABI 1 were a duplicated
baseline contract. Their successful execution did not qualify the corrected
native owner. That ABI 1 result is superseded.
2. libm3, libm3-wasm and the typed Bun loader moved into Material. The existing
native token engine and framework font descriptors became the source owner.
libm3 consumes its colors, typography, shapes, motion, state and adaptive
breakpoints. Role order is shared through the no_std native role type.
3. The corrected no_std/std facades and native Windows COSMIC adapter compiled
against libcosmic/os 7f715bc5. The custom theme scope covers inherited controls
and overlays; there is no displayed GUI or complete component proof.
4. Dynamic scalar **WASM ABI 2** was built from published Material/os
ce47e50324ce4053c4fcd08be59db55254e231f7: **113528 bytes**, SHA256
**c1abee38ea03c33b8150c14ef987906759182a6fd693c636b960922962c69ed8**.
It executed with no host imports, 49 native role names and nine variants.
A green seed changed Primary from ff65558f to ff1f6a4e. Widths 599/840/1200
returned navigation bar/collapsed rail/expanded rail.
5. The richer existing M3 controls were transferred from clean Aphrody source
96e732f1c17591389e5bb6d2dc51cb9596edff72 to Material/crates/m3. Provenance
records 23 original Rust inputs. Native/default, no-native and WASM checks
initially passed before formatting. Nineteen Rust files were then normalized
with upstream default rustfmt; original and current hashes remain recorded.
6. libm3 std now re-exports the existing component, accessibility, forms, focus,
keyboard and composition owners. Final normalized compile gates passed:
native CPU 4.68 s, no-native 1.74 s, COSMIC/MSVC 0.88 s and WASM no-native
4.62 s. The independent no_std libm3 gate passed in 0.47 s. Complete metadata
resolves 217 packages and one local token 1.4.4 owner; the earlier filtered
graph had 197 packages. No unit tests or displayed GUI were run.
7. Final Material/os consolidation **1e65a503420e4a8962b8ceaa18819eedbc280454**
was normally pushed and independently verified by remote ref. The OS lock
now pins it. It contains the 79-source-hash native consolidation receipt.
The public scalar WASM remains the exact earlier ce47e503 artifact; no fresh
scalar artifact from 1e65a503 is claimed.
8. Three unqualified Aphrody consumer manifest changes were reverted exactly.
Its Cargo.lock stayed unchanged. Original 106 token and 24 richer M3 files
remain clean/hash-identical and retained until the consumer acquisition,
lock and execution gates qualify. A local token crate package exists, but
registry publication/authentication and packaged consumer gates are incomplete.
Remaining Material work includes native web dynamic-color/MCU classic/HCT
bindings, complete component coverage, actual GUI display, full desktop/platform
behavior, original consumer migration and a final scalar WASM artifact.
Evidence: [Material transfer receipt](https://github.com/aphrody-labs/material/blob/1e65a503420e4a8962b8ceaa18819eedbc280454/crates/m3/transfer-receipt.json),
[native consolidation](https://github.com/aphrody-labs/material/blob/1e65a503420e4a8962b8ceaa18819eedbc280454/docs/native-consolidation-2026-10-13.json),
[scalar execution](../out/wasm-material/wasm/execution.json).
## Ubuntu source factory and storage
### Authenticated sources
The VPS retrieved apt **3.2.0**, dpkg **1.23.7ubuntu1** and livecd-rootfs
**26.04.35** directly from official Ubuntu resolute archives. Isolated APT
lists/cache/temp were under the selected build filesystem. The chain is
signed InRelease, authenticated Sources SHA256, and every verified DSC/tar.
Native apt-get, dpkg-source and Ubuntu keyring checks remained intact.
All three source packages were extracted. Missing detached developer keys
for dpkg/livecd-rootfs were recorded separately; Ubuntu archive authentication
remained verified. No global APT list configuration or host package installation
was performed.
Evidence: [source receipt](../out/ubuntu-source-receipt.json).
### Guarded cleanup
The remote batch reclaimed **50,080,063,488 observed bytes** across 12
allowlisted disposable classes and stopped after meeting the 50 GB threshold.
The measurement sums each successful operation's positive filesystem delta;
it does not credit concurrent restoration/source writes or promise permanent
free space.
At its final 09:22 UTC snapshot, the build filesystem had 128,892,571,648 bytes
available and root had 2,562,670,592. Later concurrent writes changed root
availability. Factory work therefore kept engine data, exec state, PID, socket,
logs and temporary files on the build disk, under the shared heavy lock.
The cleanup stopped no processes and deleted no Docker images, containers or
volumes. Factory sources and the active Shenron restoration were excluded.
It was an SSH VPS operation, not workstation media cleanup.
Evidence: [cleanup receipt](../out/factory-cleanup/receipt.json).
### Real toolchain and package artifact
A dedicated build-disk Docker engine built a real Ubuntu 26.04.1 toolchain:
- Image: sha256:03f3e7a691e043f87b5b3b64169ad762b705e973a9b0c65f901a22364f9bbb68.
- Base: ubuntu@sha256:f144425ff09be612d6d9ad965196e9cdc23dae1f42110a8a11a3e9a8198759f7.
- glibc 2.43; sbuild 0.91.2ubuntu3; mmdebstrap 1.5.7-3; uidmap
1:4.17.4-2ubuntu3; live-build 3.0~a57-1ubuntu54; livecd-rootfs 26.04.35;
germinate 2.49 and debian-cd 3.2.3.
A fresh clean sbuild unshare chroot then built livecd-rootfs 26.04.35 for amd64:
| Artifact | Bytes | SHA256 |
| --- | --- | --- |
| livecd-rootfs DEB | 148762 | 7e12304e4bdcbd33526e28d7b6c1efa6e06b334a474bb58b3be476c60a89a836 |
| clean resolute chroot tar | 475487744 | a3b18a29a32571c02fbeef1ea52a390b8bf3126b6b98493f89ccbd02ea3646bb |
The package job ended with exit 0 and released its locks. Build options were
parallel=2 nocheck; no upstream test commands were reported. Diagnostics
recorded absent /dev/console and failed /proc mounting. This successful DEB
does not qualify a distribution rootfs, ISO, desktop, boot or complete/perf image.
The engine is a bounded factory execution context, not the persistent
deployment/runner daemon.
Evidence: [toolchain receipt](../out/ubuntu-toolchain-receipt.json),
[package receipt](../out/ubuntu-package-receipt.json).
## Exact runtime and Windows package conversion
The selected Windows Bun is PE64 x64, **171403264 bytes**, SHA256
**a6f79203a9601914e86479f103c7f695330d54442169566f64e29328440d9c89**.
It reports Bun.version 1.4.4 and revision
**2ae8fc65fba1d05c423a2433b1195835109942b6**. Its reported release URL is
provenance metadata only. Stable release archive authentication and the complete
native runtime suite were not established.
The locked OS Bun source is **c209b95eb0dcc964745f4684f127abb83cbe2697**.
These revisions differ. Same-machine app compilation/execution is qualified;
staging this binary as the exact locked OS runtime is not.
The single app compiler reuses native Bun.build/embeddedFiles, the canonical
bun-cloudflare R2/authentication owner, shared content identities and existing
PE/ELF/GNU loader inspection. The APT build command is a thin adapter with its
own actual version-contract execution gate.
The final consolidated Windows apt.exe is **171469824 bytes**, SHA256
**43cacda8663021aa0640297978ef600b0d43bf8fe715d04a0f5464ee96bb2fbd**,
with 16 hashed source inputs. It executed its native version contract and:
- Converted the authenticated Ubuntu apt 3.2.0 DSC source contract.
- Inspected its authenticated source archive with 926 entries.
- Converted the original MSYS2 UCRT zlib 1.3.2-2 archive as a native DLL package.
- Emitted an actual portable WinGet manifest; native WinGet validation passed.
The zlib archive matched the primary MSYS2 publisher SHA256
841401182976d2f9e17e5c0ebaac51f2a8014140ea53d67625e91c8fb3c85ea0.
Extracted zlib1.dll retained its bytes: **127291 bytes**, PE64 x64 library,
SHA256 **11cd91171765cd65f1c3362e3df67e5f5a618a6b88c8cdec899b802eaf1c7b0d**.
Its imports resolve to Windows system owners. Detached MSYS2 signature trust
and DLL execution were not qualified.
The bridge has no native libapt installation engine or transaction backend.
The source APT version and bridge/runtime version remain distinct.
The matching WinGet installer URL now serves the exact release artifact over
the dist.aphrody.app custom domain, with public bytes and SHA256 verified.
Gzip and bounded Zstandard use runtime APIs. XZ/bzip2 require an explicitly
selected native 7-Zip executable, including the actual APT .tar.xz inspection.
That external decoder remains part of the source-inspection dependency contract.
The final build uses the current 1e65a503 Material source lock and app catalog;
its native version, source/DLL conversions and WinGet validation passed.
It supersedes the earlier 994dd5f2 artifact. Conditional R2 publication created
one content-addressed object. Public HTTPS retrieval returned HTTP 200,
171469824 bytes and the exact SHA256, with an immutable one-year cache policy.
The result qualifies public artifact delivery, not the native libapt engine
or every application behavior.
Public artifact: [apt.exe](https://dist.aphrody.app/objects/sha256/43cacda8663021aa0640297978ef600b0d43bf8fe715d04a0f5464ee96bb2fbd).
After those replacements were publicly verified, three obsolete owned output
files were removed with exact path, regular-file, size and SHA256 guards:
the two earlier apt.exe builds and the 2061-byte ABI 1 module. This retired
342936589 bytes of generated release files. Historical receipts remain.
Evidence: [runtime snapshot](../out/runtime-current/windows-x64.json),
[consolidated APT build](../out/apt-consolidated/receipt.json),
[conversion qualification](../out/apt-consolidated/qualification.json),
[public release proof](../out/apt-consolidated/public.json),
[committed artifact summary](ARTIFACTS-2026-10-13.json),
[app build contract](APP-BUILDS.md).
Primary MSYS2 proof: [publisher package page](https://packages.msys2.org/package/mingw-w64-ucrt-x86_64-zlib?repo=ucrt64).
## Applications, Cloudflare and shared R2
The inventory covers 14 native engine CLIs plus APT and Desktop. No duplicate
Cloudflare, S3, content digest or executable-ABI implementation was added.
Only explicit public asset roots are enumerated; provider/session/game/model
stores are excluded. Missing native artifacts fail qualification, including
Artcraft's unresolved executable owner. Desktop still has external workspace/
native-engine context. Other target runtime snapshots and Mach-O inspection
remain unqualified.
Cloudflare MCP created all **16 enabled Custom Domains** on the existing
**aphrody-app** Worker, with previews disabled. A fresh read-back checked each
domain and managed proxied DNS record. Public DNS resolved for every hostname;
all 16 returned **HTTPS HTTP 200 with certificate validation enabled**.
The homepage remained HTTP 200 and www redirected to it. Existing root/www
domain IDs and assets/dist R2 records were preserved. The Worker's modification
timestamp stayed unchanged; no Worker code was uploaded. These hosts serve
the existing public edge response, not independently deployed native apps.
At the recorded 11:13 UTC certificate observation, 11 new dedicated packs were
active and five pending_validation: Lightcraft, Gridcraft, Figcraft, APT and
Desktop. Existing wildcard coverage already made public HTTPS valid. No
long sleep/retry loop or certificate bypass was used.
The Material ABI 2 object was stored once under objects/sha256/<digest>.
The native R2 path verified source and remote SHA256; public retrieval returned
HTTP 200, 113528 bytes and the same digest. A second conditional upload returned
created=false and verified the existing object. This proves one shared WASM
object and does not claim all application assets have been uploaded.
Public object:
[Material ABI 2 WASM](https://assets.aphrody.app/objects/sha256/c1abee38ea03c33b8150c14ef987906759182a6fd693c636b960922962c69ed8).
Evidence: [domain receipt](../out/app-domains/receipt.json),
[domain contract](APP-DOMAINS.md),
[R2 upload](../out/wasm-material/wasm/r2.json),
[public delivery](../out/wasm-material/wasm/public.json),
[deduplication](../out/wasm-material/wasm/dedup.json).
## Deployment control and workflow preparation
The prepared Rust control-plane profile uses Komodo Core/Periphery 2.3.3,
FerretDB 2.7.0 with PostgreSQL DocumentDB, Forgejo 15.0.10 and Runner 13.2.0.
Six exact OCI image digests and upstream provenance are recorded.
MongoDB was excluded for the observed kernel compatibility limit; the official
FerretDB integration was selected instead. Native JSON/YAML parsing and the
Compose template validation passed.
No image layers were pulled by this deployment-profile batch. Persistent
deployment/CI Docker contexts, authenticated service health, first admin,
forge/runner registration, native job image and live workflows are unqualified.
The factory daemon and existing production/global Docker contexts are not the
deployment owner. No private control/backend listener was exposed.
Evidence: [control-plane profile](../infra/control-plane.json),
[deployment contract](DEPLOYMENT-CONTROL.md).
The [Rust systems catalog](RUST-SYSTEMS.md) verifies canonical package names,
allocator locking, no_std/alloc feature boundaries and the current native versus
future bare-metal architecture. It adds no dependencies or new runtime claims.
## Gates, coordination and publication
Native Bun source gates passed after app integration: check covered 28 files,
scoped lint had no warnings, and format checks passed. Root later extended its
lint/format scripts to cover infra. Material compiler gates are listed above;
no new behavior tests were authored or run. Existing upstream test sources
were preserved during the owner transfer. A compile check, scalar execution, source DEB,
DNS/TLS probe and template validation have their stated scopes.
A separate thread reported workstation episode deletion. New heavy jobs were
temporarily suspended while our command payloads were checked; none targeted
those episode paths. A shared app-server PID does not identify the responsible
task. Our cleanup covered the explicit SSH VPS batch and the three guarded
obsolete release files above. No protected .local/provider/media deletion or
recovery was performed by this batch.
| Publication | Verified state |
| --- | --- |
| APT owner main | 745fc126218c7cb3df48b22bbc7c7098fa2f7966, normal push/ref verified |
| Earlier Material/os | ce47e50324ce4053c4fcd08be59db55254e231f7, source of public ABI 2 WASM |
| Final Material/os | 1e65a503420e4a8962b8ceaa18819eedbc280454, normal push/ref verified |
| Bun OS report/consolidation | Pending root commit/push; obtain final identity from Git log |
| Original Aphrody consumers | Migration deferred; unqualified manifest changes reverted |
| Registry token crate | Local package only; publication not qualified |
| Native apps/control/forge | Not deployed |
This report belongs to the consolidation batch. Its commit identity is obtained
from the repository's Git log, rather than embedding a self-referential SHA.
Ignored out receipts contain local/VPS execution evidence and are not silently
published with documentation.
## Unresolved objective
Complete source export/LFS/capacity proof; the source-matched OS Bun runtime;
complete COSMIC/libcosmic and hardware boot; distribution rootfs/full/perf
images; native Windows libapt and package transactions; complete conversion and
compression/runtime dependency closure; final Material web/native GUI and
consumer migration; remaining standalone apps and cross-target ABIs; live
control/forge/runner deployment; full Bun/JSC/desktop WASM; performance
measurements; and identification/integration of **9937-389B** remain open.
+211
View File
@@ -0,0 +1,211 @@
# Pinned source export
`factory/source-export.ts` exports the full source mounts in `sources.lock.json`.
It is a library for the factory orchestrator. Import `exportPinnedSources(output,
options)`; running this module alone does not start a download.
The output contains `sources/` and `sources.receipt.json`. A receipt is complete
only after every mount, gitlink, source file and LFS payload has passed its gates.
An unavailable object, unsupported source path, deadline or truncated metadata
produces an incomplete receipt. The caller must refuse packaging that receipt.
`expectedRepositories` records the planned mount count. File counts, byte counts,
the final pointer list and tree hash describe the entire tree only when
`integrityComplete` is true; earlier progress receipts do not claim a completed
filesystem inventory.
## Acquisition and topology
An archive is acquired once per `repository@revision`. Repeated mounts reuse it
and retain their own parent, submodule path and revision. Different revisions
remain separate acquisitions. Archive cache entries are rehashed before reuse.
There is no clone, fetch, checkout, install, source build or execution of source
scripts in this helper.
Local Git commands disable fsmonitor, hooks, pagers and lazy object fetching.
Partial/promisor repositories are refused before object reads, including on
older Git versions that do not implement the lazy-fetch environment gate.
There are two acquisition routes:
- An explicitly configured clean checkout or existing Git object repository.
Git resolves the exact commit and streams its recursive file/gitlink inventory.
A private bare view in the export cache reads the existing object store. Its
`info/attributes` disables `export-ignore` and `export-subst`, so committed
files are retained without changing the owner's attributes or metadata.
The extracted file count and gitlinks must match the inventory. This follows
the [Git archive attribute contract](https://git-scm.com/docs/git-archive#_attributes).
- GitHub commit and recursive Git-tree metadata, followed by the commit's
codeload archive. Every extracted file and symlink is checked against its Git
blob SHA-1, size and mode; every expected blob must be present. Truncated trees
are refused before archive acquisition. The [GitHub tree API](https://docs.github.com/en/rest/git/trees#get-a-tree)
limits recursive results to 100,000 entries and 7 MB. A large icons tree needs
a qualified existing local Git repository rather than an assumed complete API
result. Archives that omit committed files or automatically expand LFS bytes
fail this route's Git-blob check; use the local Git route and an LFS owner.
The archive SHA-256 records the acquired bytes. It is not a publisher checksum
or a substitute for the commit/tree checks. GitHub compression can change while
the pinned source files remain identical, as described in its
[source archive contract](https://docs.github.com/en/repositories/working-with-files/using-files/downloading-source-code-archives).
## Capacity gate
Provide `estimates` for every distinct `repository@revision` before invoking the
helper. Each estimate contains positive `archiveBytes`, nonnegative
`expandedBytes`, and positive `entries`. Values are safe integer upper bounds.
`expandedBytes` includes the final materialized LFS bytes; `entries` includes
files, directories and symlinks. Measurements belong to the source/factory
owner, using an existing inventory or a previously measured archive. A guessed
small estimate cannot qualify the icons tree.
Before any Git inventory or network acquisition, the helper measures available
bytes and inodes on the output and cache filesystems. The budget includes every
logical mount, one retained archive and an LFS cache per distinct acquisition,
metadata allowance, allocation-block overhead, and reserves. Defaults retain
2 GiB and 100,000 inodes on each filesystem. Windows inode counts are recorded
as `null`, because NTFS allocates file records dynamically; its file-allocation
cost remains in the byte budget. Acquisition and extraction also enforce the
provided upper bounds while streaming.
This gate does not make an icons acquisition cheap. Inventory its archive size,
expanded bytes, file count, destination mount and free inodes before scheduling
it on the VPS. No new icons clone is needed by this helper. Existing metadata,
including metadata whose deletion was review-rejected, is read-only input and
is never cleaned, rewritten or recreated.
## LFS ownership
Pointers follow the [Git LFS specification](https://github.com/git-lfs/git-lfs/blob/main/docs/spec.md):
canonical v1 text, a lowercase SHA-256 OID and an exact byte size. Unknown pointer
extensions remain incomplete because their transformations require an owner
adapter.
A LFS payload can come from:
1. The corresponding regular tracked file in a clean configured checkout whose
`HEAD` equals the pinned revision. An unsmudged pointer is not accepted as its
payload.
2. The `lfsOwner(request)` callback, which supplies a bounded readable stream.
The request carries repository, revision, path, OID, size and cancellation
signal. The owner must authorize that source object and supply only source
payloads. The exporter never reads a provider's credential, session, cookie,
database or model store.
Both routes verify SHA-256 and byte size into a private incoming file. Only then
is the source pointer atomically replaced. Verified LFS objects are reused per
repository/OID. A missing local payload falls back to an explicitly configured
owner. A failure leaves the source pointer and a sanitized issue in the receipt.
`createLfsBatchOwner({ endpoint, headers, allowedDownloadHosts })` supplies the
[Git LFS basic Batch API](https://github.com/git-lfs/git-lfs/blob/main/docs/api/batch.md)
adapter. The credentials owner supplies request headers in memory. The adapter
checks the returned OID/size, accepts HTTPS only, requires an exact approved
object hostname, and refuses redirects. Batch authorization is never forwarded
to an object host; only that object's action headers are used. Tokens, signed
URLs, response bodies and subprocess stderr are never logged or stored in
receipts. Inject this callback from the trusted factory orchestrator, rather
than placing secrets in source-export JSON configuration.
## Filesystem and job boundaries
The streaming tar/gzip reader handles Git's regular files, directories,
symlinks, PAX headers and GNU long names. It refuses traversal, `.git` metadata,
special files, hardlinks and unsupported sparse
formats. It retains a symlink's exact target text, including an external target,
and never copies through that link. Source parents and local LFS paths must be
real directories; symbolic ancestors cannot redirect a nested source mount.
Files use normalized Git executable permissions; source directories are
readable by the container's runtime user.
`protectedRoots` adds explicitly isolated inputs to the path guard. The home
`.aphrody`, `.codex` and `.claude` stores and configured `APHRODY_HOME` are also
refused automatically. Committed repository plugin/instruction directories
such as Bun's `.claude` remain source files; isolated home sessions are never
copied. Archive export
reads committed Git objects; it never copies untracked, ignored or working-tree
directories. Clean checks cover tracked files; ignored/generated inputs are
neither included nor deleted. Worktrees with tracked changes are refused.
Cleanliness checks disable external clean/process/smudge filters. A materialized
LFS file reported as modified under that setting is accepted only when its raw
SHA-256 and size match the committed `HEAD` pointer. Custom transformations
cannot make arbitrary changed working files qualify as clean.
Native Windows paths and symlink creation must be supported by that host;
unrepresentable filenames or insufficient symlink privileges remain incomplete.
Executable bits are checked after writing; a filesystem that cannot retain
the required POSIX modes cannot certify an executable source tree. Native
Windows can orchestrate materialization on the qualified SSH/Linux factory.
The helper never starts WSL.
The output uses the same exclusive `.assembling` lock as the current distribution
assembler. The cache has a separate exclusive `.source-export.lock`. Claims
record a PID and nonce and are released only by their owner; existing locks are
not stolen. Do not run two assemblers against one output. Use a fresh output
after a partial source extraction; the entire `sources/` root must initially be
empty, so unrelated pre-existing files cannot be certified. Empty gitlink
directories produced by the current export can receive their pinned child.
Verified archive/LFS cache objects can be
reused. No recursive cleanup or cache pruning is performed. Only a newly created
failed incoming stream file is removed by its creator.
The default job deadline is 30 minutes. Git metadata commands are bounded at
120 seconds, recursive inventories and archive commands at 600 seconds, with
the whole-job signal able to cancel them earlier. Callback waits and streamed
reads obey the whole-job cancellation signal. The factory must also launch its
caller in a bounded background `Bun.spawn` job, retain the live PID/handle and
log paths, and avoid synchronous long shell commands. On the VPS acquire the
shared native `flock -n` heavy lock from the host's qualified factory config;
the current qualified path is `/home/ubuntu/build/locks/heavy.lock`. Keep the
active factory/restore trees and unpushed sources protected.
## Distribution integration
The root owner can replace the body of `assembleSources` with a call to
`exportPinnedSources`. Extend its configuration with the estimates, cache and
protected-root fields; resolve checkout/object-repository paths explicitly.
The helper accepts keys in this order: `repository@revision`, source id,
repository. A configured repeated mount can supply the acquisition for its
matching peers. Put clean worktrees in `checkouts`; put existing bare Git
repositories in `repositories`. A bare repository supplies committed archive
objects, never a working-tree LFS payload.
```ts
import { exportPinnedSources } from "../factory/source-export.ts";
const receipt = await exportPinnedSources(output, {
lock: await readLock(),
checkouts: config.checkouts,
repositories: config.sourceRepositories,
estimates: config.sourceEstimates,
cache: config.sourceCache,
protectedRoots: config.protectedRoots,
lfsOwner: configuredLfsOwner,
githubHeaders: configuredGithubHeaders,
});
if (!receipt.complete) throw new Error("Source export incomplete; inspect sources.receipt.json");
return receipt;
```
This snippet belongs in the existing root-owned assembler after extending its
configuration; it is not a standalone job. The current assembler/container files
are not changed by this helper. Retain `stageRuntime`'s exact source-lock and
tree-hash gates. The helper's final tree SHA-256 matches the existing
`container/source-integrity.ts` contract, with cancellation checks added. It
hashes paths, entry types, file bytes and symlink target text; permissions are
not part of that existing hash contract.
Keep the source cache outside the staged Docker context, or add `.source-cache`
to the root-owned `.dockerignore`. Only `sources/` and its complete receipt
belong in the image; cache archives, temporary bare views, credentials and
incomplete outputs do not.
Scoped qualification commands:
```text
bun check factory/source-export.ts --threads=2 --no-pretty
bun lint --deny-warnings factory/source-export.ts
bun fmt --check -c bun-format.json factory/source-export.ts
```
These commands qualify source code only. Full source acquisition, an LFS
provider transfer, tar extraction, Docker packaging and production execution
require their own factory receipts and have not been established by a type or
format check.
+72
View File
@@ -0,0 +1,72 @@
# Ubuntu OS factory
Run the Bun owner on the selected Linux, SSH or Docker host. An explicit
factory root owns source jobs, temporary files, isolated APT state, logs,
Docker data, execution state, socket and receipts.
## Source retrieval
Source jobs authenticate each Ubuntu InRelease with the archive keyring,
then compare every DSC and archive size/SHA256 with the authenticated
Sources record before extraction. dpkg-source retains its default signature
checks and requires strong checksums. Uploader signature messages are
recorded separately from archive authentication.
```sh
bun factory/ubuntu.ts status --root /ABSOLUTE/FACTORY
bun factory/ubuntu.ts sources apt dpkg livecd-rootfs --root /ABSOLUTE/FACTORY
```
Each mutation creates a fresh job directory and records live child PIDs,
logs, finite deadlines and a receipt. The factory has an exclusive lock
and requires 50 GiB free. Existing locks are preserved. All APT configuration,
lists, cache and temporary files stay in the job; source retrieval installs
no packages. Ubuntu source package versions and the Salsa APT fork revisions
have independent provenance.
## Isolated toolchain
After the cleanup owner releases its build slot, request the dedicated
Docker engine. Its data root, execution root, socket, PID file, logs and
temporary files are on the factory filesystem. Bridge, forwarding and
iptables management are disabled; containerd has factory namespaces.
```sh
bun factory/ubuntu.ts engine --root /ABSOLUTE/FACTORY
bun factory/ubuntu.ts status --root /ABSOLUTE/FACTORY
bun factory/ubuntu.ts toolchain --root /ABSOLUTE/FACTORY
```
The toolchain owner verifies the Docker endpoint and filesystem, acquires
the configured shared heavy lock, resolves an immutable official Ubuntu
base digest, and installs sbuild, live-build, livecd-rootfs, germinate,
debian-cd and image utilities through Ubuntu APT. The receipt records the
image identity, package versions, OS and libc. Installed tools do not prove
a clean sbuild chroot, built rootfs, ISO or complete Bun OS image.
## Package and rootfs builds
Native package builds use the Ubuntu-recommended unshare backend with mmdebstrap
and uidmap. Each build owns its sbuild configuration, HOME, cache, chroot
tarball, temporary paths and hashed binary artifacts. The configured Ubuntu
image user supplies its namespace ID maps; only verified source files and
the fresh job are mounted into each package build. Package builds use
`DEB_BUILD_OPTIONS=parallel=2 nocheck`. Receipts record native namespace
mount diagnostics separately from package and chroot archive hashes. The optional --chroot
selects a pre-provisioned schroot for cross-building. Rootfs builds
require prepared live-build configuration, livecd-rootfs customizations,
seeds, package selection and kernel artifacts. Inputs reside inside the
mounted factory root. Container commands bound CPU, memory, PIDs and time.
```sh
bun factory/ubuntu.ts package /ABSOLUTE/FACTORY/sources/apt.dsc --root /ABSOLUTE/FACTORY
bun factory/ubuntu.ts rootfs --root /ABSOLUTE/FACTORY --config /ABSOLUTE/FACTORY/live-build
```
Inspect and hash produced artifacts before product qualification. WSLg's
Azure Linux system distro remains a separate target.
Primary guidance: [Ubuntu source retrieval](https://documentation.ubuntu.com/project/contributors/setup/get-the-source-of-a-package/),
[Ubuntu local package builds](https://ubuntu.com/project/docs/contributors/building/build-packages-locally),
[Ubuntu image process](https://ubuntu.com/project/docs/how-ubuntu-is-made/processes/image-builds/),
and [separate Docker daemons](https://docs.docker.com/reference/cli/dockerd/#run-multiple-daemons).
+598
View File
@@ -0,0 +1,598 @@
{
"schemaVersion": 1,
"minimumFreeBytes": 53687091200,
"jobs": 2,
"targets": [
{
"id": "linux-aphrody-bun",
"sourceId": "linux",
"revision": "47f637c546bf4bb274c7149815ed66d748e4f325",
"branch": "aphrody-bun",
"host": "linux",
"architecture": "x64",
"abi": "linux-kernel",
"license": "GPL-2.0-only",
"ownerFiles": [
"Makefile",
"drivers/misc/Kconfig",
"Documentation/kbuild/kbuild.rst",
"Documentation/rust/quick-start.rst",
"scripts/rust_is_available.sh"
],
"tools": [
"git",
"make",
"gcc",
"g++",
"ld",
"bison",
"flex",
"perl",
"pkg-config",
"clang",
"clang++",
"ld.lld",
"llvm-ar",
"llvm-nm",
"llvm-strip",
"llvm-objcopy",
"llvm-objdump",
"llvm-readelf",
"rustc",
"bindgen"
],
"pkgConfig": ["openssl", "libelf"],
"requirements": [
"An explicit x86_64 kernel configuration with CONFIG_BUN_ACCEL=y or m",
"Kernel toolchain and optional configuration dependencies supplied by the host",
"LLVM toolchain, matching Rust standard library sources and libclang for bindgen; source rustavailable guard must succeed"
],
"steps": [
{
"copy": "{kernelConfig}",
"to": "{out}/build/.config"
},
{
"cmd": [
"make",
"-C",
"{source}",
"O={out}/build",
"ARCH=x86",
"LLVM=1",
"RUSTC=rustc",
"BINDGEN=bindgen",
"rustavailable"
],
"timeoutMs": 30000
},
{
"cmd": [
"make",
"-C",
"{source}",
"O={out}/build",
"ARCH=x86",
"olddefconfig",
"LLVM=1",
"RUSTC=rustc",
"BINDGEN=bindgen"
],
"timeoutMs": 300000,
"checkKernelConfig": true
},
{
"cmd": [
"make",
"-C",
"{source}",
"O={out}/build",
"ARCH=x86",
"-j2",
"bzImage",
"modules",
"LLVM=1",
"RUSTC=rustc",
"BINDGEN=bindgen"
],
"timeoutMs": 14400000
},
{
"cmd": [
"make",
"-C",
"{source}",
"O={out}/build",
"ARCH=x86",
"INSTALL_MOD_PATH={out}/modules",
"modules_install",
"LLVM=1",
"RUSTC=rustc",
"BINDGEN=bindgen"
],
"timeoutMs": 1800000
},
{
"cmd": [
"make",
"-C",
"{source}",
"O={out}/build",
"ARCH=x86",
"INSTALL_HDR_PATH={out}/headers",
"headers_install",
"LLVM=1",
"RUSTC=rustc",
"BINDGEN=bindgen"
],
"timeoutMs": 300000
}
],
"artifacts": ["build/arch/x86/boot/bzImage", "build/vmlinux", "build/.config"],
"unqualified": [
"Boot on the selected physical/virtual host",
"/dev/bun_accel permissions and runtime integration",
"ARM64 needs its own owner configuration and recipe"
]
},
{
"id": "wsl-windows",
"sourceId": "wsl",
"revision": "341f1ddf76f089ff6351590b03f92bad30041725",
"host": "win32",
"architecture": "x64",
"abi": "msvc-ucrt",
"license": "MIT",
"ownerFiles": ["CMakeLists.txt", ".vsconfig", "cmake/FindLINUXBUILD.cmake", "doc/docs/dev-loop.md"],
"tools": ["git", "cmake", "dotnet"],
"pkgConfig": [],
"requirements": [
"Visual Studio 2022 v143 workloads declared by .vsconfig",
"Windows SDK 10.0.26100.0",
"Developer Mode or equivalent permission to create build links",
"Pinned NuGet packages, LinuxSDK and native Clang guest-helper cross compiler"
],
"steps": [
{
"cmd": [
"cmake",
"-S",
"{source}",
"-B",
"{out}/build",
"-G",
"Visual Studio 17 2022",
"-A",
"x64",
"-DCMAKE_SYSTEM_VERSION=10.0.26100.0",
"-DCMAKE_BUILD_TYPE=Release"
],
"timeoutMs": 1800000
},
{
"cmd": ["cmake", "--build", "{out}/build", "--config", "Release", "--parallel", "2"],
"timeoutMs": 14400000
}
],
"artifacts": ["build/bin/x64/Release/wsl.exe"],
"unqualified": [
"Installer/package signing",
"Windows service and WSL1/WSL2 execution",
"No deployment, WSL startup or registration is performed by this recipe"
]
},
{
"id": "wslg-system",
"sourceId": "wslg",
"revision": "6942bb534b537302a7a22617261d9274db3cfa37",
"host": "linux",
"architecture": "x64",
"abi": "gnu-glibc",
"license": "MIT",
"ownerFiles": ["Dockerfile", "CONTRIBUTING.md", "build-and-export.sh"],
"tools": ["git", "docker", "go"],
"pkgConfig": [],
"externalSources": [
{
"path": "vendor/FreeRDP",
"repository": "https://github.com/microsoft/FreeRDP-mirror",
"buildArg": "FREERDP_COMMIT"
},
{
"path": "vendor/weston",
"repository": "https://github.com/microsoft/weston-mirror",
"buildArg": "WESTON_COMMIT"
},
{
"path": "vendor/pulseaudio",
"repository": "https://github.com/microsoft/PulseAudio-mirror",
"buildArg": "PULSEAUDIO_COMMIT"
},
{
"path": "vendor/mesa",
"repository": "https://gitlab.freedesktop.org/mesa/mesa",
"buildArg": "MESA_VERSION"
},
{
"path": "vendor/DirectX-Headers-1.0",
"repository": "https://github.com/microsoft/DirectX-Headers",
"buildArg": "DIRECTX_HEADERS_VERSION"
}
],
"requirements": [
"Five separately pinned vendor Git sources; this WSLg source has no gitlinks",
"An immutable Azure Linux base image digest and explicit WSLg version",
"A live Linux Docker daemon on the selected factory",
"Network access to the pinned hcsshim tar2ext4 v0.13.0 module"
],
"steps": [
{
"cmd": [
"docker",
"build",
"--file",
"{source}/Dockerfile",
"--tag",
"{image}",
"--build-arg",
"MARINER_IMAGE={baseImage}",
"--build-arg",
"WSLG_ARCH=x86_64",
"--build-arg",
"WSLG_VERSION={version}",
"--build-arg",
"WSLG_COMMIT={revision}",
"--build-arg",
"DIRECTX_HEADERS_VERSION={DIRECTX_HEADERS_VERSION}",
"--build-arg",
"FREERDP_COMMIT={FREERDP_COMMIT}",
"--build-arg",
"MESA_VERSION={MESA_VERSION}",
"--build-arg",
"PULSEAUDIO_COMMIT={PULSEAUDIO_COMMIT}",
"--build-arg",
"WESTON_COMMIT={WESTON_COMMIT}",
"{source}"
],
"timeoutMs": 14400000
},
{
"cmd": ["docker", "create", "--name", "{container}", "{image}"],
"timeoutMs": 60000
},
{
"cmd": ["docker", "export", "--output", "{out}/system_x64.tar", "{container}"],
"timeoutMs": 1800000
},
{
"cmd": [
"go",
"run",
"github.com/Microsoft/hcsshim/cmd/tar2ext4@v0.13.0",
"-vhd",
"-i",
"{out}/system_x64.tar",
"-o",
"{out}/system_x64.vhd"
],
"timeoutMs": 1800000
},
{
"cmd": ["docker", "rm", "{container}"],
"timeoutMs": 60000
}
],
"artifacts": ["system_x64.tar", "system_x64.vhd"],
"unqualified": [
"Windows RDP plugin built by its separate Visual Studio owner",
"WSL host/system-distro integration and GPU/audio behavior",
"Azure Linux owner image is not an Ubuntu rootfs"
]
},
{
"id": "wsl-kernel",
"sourceId": "wsl-kernel",
"revision": "e5043002ed6f0b0eb17a0fb9efafa794045853ed",
"branch": "aphrody-wsl-6.18",
"host": "linux",
"architecture": "x64",
"abi": "linux-kernel",
"license": "GPL-2.0-only",
"ownerFiles": [
"Makefile",
"Microsoft/config-wsl",
"Microsoft/scripts/gen_artifacts_vhdx.sh",
"README.md",
"Documentation/rust/quick-start.rst",
"scripts/rust_is_available.sh",
"Microsoft/aphrody-wsl.config",
"arch/x86/configs/aphrody_wsl_defconfig",
"drivers/misc/Kconfig"
],
"tools": [
"git",
"make",
"gcc",
"g++",
"ld",
"bison",
"flex",
"perl",
"pkg-config",
"pahole",
"cpio",
"rsync",
"mke2fs",
"qemu-img",
"bash",
"clang",
"clang++",
"ld.lld",
"llvm-ar",
"llvm-nm",
"llvm-strip",
"llvm-objcopy",
"llvm-objdump",
"llvm-readelf",
"rustc",
"bindgen"
],
"pkgConfig": ["openssl", "libelf"],
"requirements": [
"Pinned arch/x86/configs/aphrody_wsl_defconfig with CONFIG_RUST=y and CONFIG_BUN_ACCEL=y",
"Host kernel build dependencies from the owner README",
"Space for modules, UAPI headers, perf and an ext4/VHDX artifact",
"LLVM toolchain, matching Rust standard library sources and libclang for bindgen; source rustavailable guard must succeed"
],
"steps": [
{
"copy": "{source}/arch/x86/configs/aphrody_wsl_defconfig",
"to": "{out}/build/.config"
},
{
"cmd": [
"make",
"-C",
"{source}",
"O={out}/build",
"ARCH=x86",
"LLVM=1",
"RUSTC=rustc",
"BINDGEN=bindgen",
"rustavailable"
],
"timeoutMs": 30000
},
{
"cmd": [
"make",
"-C",
"{source}",
"O={out}/build",
"ARCH=x86",
"olddefconfig",
"LLVM=1",
"RUSTC=rustc",
"BINDGEN=bindgen"
],
"timeoutMs": 300000,
"checkKernelConfig": true
},
{
"cmd": [
"make",
"-C",
"{source}",
"O={out}/build",
"ARCH=x86",
"-j2",
"bzImage",
"modules",
"LLVM=1",
"RUSTC=rustc",
"BINDGEN=bindgen"
],
"timeoutMs": 14400000
},
{
"cmd": [
"make",
"-C",
"{source}",
"O={out}/build",
"ARCH=x86",
"INSTALL_MOD_PATH={out}/modules",
"modules_install",
"LLVM=1",
"RUSTC=rustc",
"BINDGEN=bindgen"
],
"timeoutMs": 1800000
},
{
"cmd": [
"make",
"-C",
"{source}",
"O={out}/build",
"ARCH=x86",
"INSTALL_HDR_PATH={out}/headers",
"headers_install",
"LLVM=1",
"RUSTC=rustc",
"BINDGEN=bindgen"
],
"timeoutMs": 300000
},
{
"cmd": [
"make",
"-C",
"{source}/tools/perf",
"O={out}/perf-build",
"-j2",
"NO_JEVENTS=1",
"NO_JVMTI=1",
"NO_LIBTRACEEVENT=1",
"install",
"DESTDIR={out}/perf",
"prefix=/",
"LLVM=1",
"RUSTC=rustc",
"BINDGEN=bindgen"
],
"timeoutMs": 1800000
},
{
"cmd": [
"make",
"--no-print-directory",
"-s",
"-C",
"{source}",
"O={out}/build",
"kernelrelease",
"LLVM=1",
"RUSTC=rustc",
"BINDGEN=bindgen"
],
"capture": "kernelRelease",
"timeoutMs": 60000
},
{
"cmd": [
"bash",
"{source}/Microsoft/scripts/gen_artifacts_vhdx.sh",
"{out}/modules",
"{out}/headers",
"{out}/perf",
"{kernelRelease}",
"{out}/modules.vhdx"
],
"timeoutMs": 1800000
}
],
"artifacts": ["build/arch/x86/boot/bzImage", "build/vmlinux", "modules.vhdx"],
"unqualified": [
"The selected Windows WSL kernel boot and modules mount",
"No .wslconfig mutation or WSL startup",
"ARM64 requires a separate configuration"
]
},
{
"id": "apt-gnu",
"sourceId": "apt",
"revision": "745fc126218c7cb3df48b22bbc7c7098fa2f7966",
"host": "linux",
"architecture": "x64",
"abi": "gnu-glibc",
"license": "GPL-2.0-or-later",
"ownerFiles": ["CMakeLists.txt", "apt-pkg/CMakeLists.txt", "debian/control", "README.md"],
"tools": ["git", "cmake", "ninja", "g++", "perl", "triehash", "grep", "awk", "pkg-config", "dpkg-architecture"],
"pkgConfig": ["openssl", "zlib", "liblzma", "liblz4", "libzstd", "libxxhash"],
"requirements": [
"C++23 toolchain",
"Berkeley DB, bzip2, Iconv and large-file headers/libraries",
"Perl Dpkg module and trusted Debian package/source metadata"
],
"steps": [
{
"cmd": [
"cmake",
"-S",
"{source}",
"-B",
"{out}/build",
"-G",
"Ninja",
"-DCMAKE_BUILD_TYPE=Release",
"-DWITH_DOC=OFF",
"-DWITH_TESTS=OFF",
"-DUSE_NLS=OFF",
"-DCMAKE_INSTALL_PREFIX=/usr"
],
"timeoutMs": 300000
},
{
"cmd": ["cmake", "--build", "{out}/build", "--parallel", "2"],
"timeoutMs": 7200000
},
{
"cmd": ["cmake", "--install", "{out}/build"],
"env": {
"DESTDIR": "{out}/stage"
},
"timeoutMs": 300000
}
],
"artifacts": ["stage/usr/bin/apt", "stage/usr/bin/apt-get", "stage/usr/bin/apt-cache"],
"unqualified": [
"Archive trust/signature verification workload",
"Trusted dpkg transactions against an explicit target root",
"The GNU binaries are not Windows PE artifacts"
]
},
{
"id": "apt-windows-msvc",
"sourceId": "apt",
"revision": "745fc126218c7cb3df48b22bbc7c7098fa2f7966",
"host": "win32",
"architecture": "x64",
"abi": "msvc-ucrt",
"license": "GPL-2.0-or-later",
"ownerFiles": [
"CMakeLists.txt",
"CMake/FindLFS.cmake",
"apt-pkg/CMakeLists.txt",
"apt-pkg/contrib/fileutl.cc",
"apt-pkg/contrib/mmap.cc",
"apt-pkg/deb/dpkgpm.cc"
],
"tools": ["git", "cmake", "ninja"],
"pkgConfig": [],
"portRequired": true,
"requirements": [
"Port CMake POSIX statfs/endian/signal/LFS checks, Perl/Dpkg/id commands, grep/awk generators and GNU version scripts to native Windows generators and DLL exports",
"Implement UCRT 64-bit file offsets, Win32 locking/mapping/process/pipes, Unicode paths and socket/event operations in the APT owners",
"Supply MSVC/UCRT builds of Iconv, Berkeley DB, OpenSSL, zlib, bzip2, liblzma, lz4, zstd and xxhash through an explicit CMake toolchain",
"Supply native signature verifier and a separately ported package-transaction backend; Linux dpkg and ELF payloads are not Windows executables",
"Compile and qualify libapt-pkg plus apt.exe from the ported revision; the Bun conversion frontend is a separate artifact"
],
"steps": [
{
"cmd": [
"bun",
"msvc",
"--arch",
"x64",
"exec",
"--",
"cmake",
"-S",
"{source}",
"-B",
"{out}/build",
"-G",
"Ninja",
"-DCMAKE_BUILD_TYPE=Release",
"-DCMAKE_TOOLCHAIN_FILE={windowsToolchain}",
"-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL",
"-DWITH_DOC=OFF",
"-DWITH_TESTS=OFF",
"-DUSE_NLS=OFF"
],
"timeoutMs": 300000
},
{
"cmd": ["bun", "msvc", "--arch", "x64", "exec", "--", "cmake", "--build", "{out}/build", "--parallel", "2"],
"timeoutMs": 7200000
}
],
"artifacts": ["build/bin/apt.exe"],
"unqualified": [
"This pinned official APT revision has no native UCRT/MSVC port",
"Proposed Windows steps stay non-executable until the source owner implements and pins that port"
]
}
]
}
+472
View File
@@ -0,0 +1,472 @@
#!/usr/bin/env bun
// SPDX-License-Identifier: MIT
import { mkdir, open, lstat, realpath, statfs, unlink } from "node:fs/promises";
import { dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
import { readLock } from "../src/distribution.ts";
import { abi } from "../packages/package-converter/src/abi.ts";
interface Step {
cmd?: string[];
copy?: string;
to?: string;
timeoutMs?: number;
capture?: string;
env?: Record<string, string>;
checkKernelConfig?: boolean;
}
interface NativeTarget {
id: string;
sourceId: string;
revision: string;
branch?: string;
host: "linux" | "win32";
architecture: "x64";
abi: string;
license: string;
ownerFiles: string[];
tools: string[];
pkgConfig: string[];
externalSources?: { path: string; repository: string; buildArg: string }[];
portRequired?: boolean;
requirements: string[];
steps: Step[];
artifacts: string[];
unqualified: string[];
}
interface TargetCatalog {
schemaVersion: 1;
minimumFreeBytes: number;
jobs: 2;
targets: NativeTarget[];
}
interface NativeConfig {
baseImage?: string;
version?: string;
windowsToolchain?: string;
externalPins?: Record<string, string>;
}
export interface NativePlan {
target: NativeTarget;
variables: Record<string, string>;
steps: Step[];
minimumFreeBytes: number;
}
const packageRoot = resolve(import.meta.dir, "..");
const usage = `Pinned native source-owner factory
plan TARGET [--source CHECKOUT --out NEW_DIRECTORY --config FILE --kernel-config FILE]
status TARGET --source CHECKOUT --out NEW_DIRECTORY [--config FILE --kernel-config FILE]
build TARGET --source CHECKOUT --out NEW_DIRECTORY [--factory DIRECTORY --config FILE --kernel-config FILE]
Targets: linux-aphrody-bun, wsl-windows, wslg-system, wsl-kernel, apt-gnu, apt-windows-msvc
Run Linux targets on their Linux factory. Native Windows recipes never start WSL.
APT MSVC steps are a source-port plan; the pinned official revision cannot execute them.`;
function contained(root: string, path: string): string {
const target = resolve(root, path);
const rel = relative(root, target);
if (isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`))
throw new Error("Factory path escapes its selected root");
return target;
}
async function run(
cmd: string[],
cwd: string,
timeout: number,
log?: string,
env?: Record<string, string>,
): Promise<string> {
const chunks: Uint8Array[] = [];
let bytes = 0;
const writer = log ? await open(log, "wx") : undefined;
let writes = Promise.resolve();
await using child = Bun.spawn({
cmd,
cwd,
env: { ...Bun.env, ...env },
stdin: "ignore",
stdout: "pipe",
stderr: "pipe",
timeout,
windowsHide: true,
});
const consume = async (stream: ReadableStream<Uint8Array>, stdout: boolean) => {
for await (const chunk of stream) {
bytes += chunk.length;
if (bytes > (log ? 32 * 1024 * 1024 : 1024 * 1024)) {
child.kill();
throw new Error("Factory command log exceeds byte limit");
}
if (writer) {
writes = writes.then(() => writer.writeFile(chunk));
await writes;
}
if (stdout && !log) chunks.push(chunk);
}
};
try {
const results = await Promise.allSettled([consume(child.stdout, true), consume(child.stderr, false), child.exited]);
for (const result of results) if (result.status === "rejected") throw result.reason;
const status = results[2]!;
if (status.status !== "fulfilled" || status.value !== 0)
throw new Error(`Factory command failed: ${cmd[0]}${log ? `; log: ${log}` : ""}`);
const length = chunks.reduce((total, chunk) => total + chunk.length, 0);
const data = new Uint8Array(length);
let offset = 0;
for (const chunk of chunks) {
data.set(chunk, offset);
offset += chunk.length;
}
return new TextDecoder().decode(data).trim();
} finally {
await writer?.close();
}
}
async function cleanPin(source: string, revision: string) {
if (!/^[a-f0-9]{40}$/.test(revision)) throw new Error("Invalid exact source pin");
const head = await run(["git", "-C", source, "rev-parse", "HEAD"], packageRoot, 15_000);
if (head !== revision) throw new Error(`Source HEAD differs from ${revision}`);
const changes = await run(
["git", "-C", source, "status", "--porcelain", "--untracked-files=normal"],
packageRoot,
15_000,
);
if (changes) throw new Error("Source checkout has concurrent/uncommitted work; select the exact clean owner source");
}
async function digest(path: string) {
const file = Bun.file(path);
if (!(await file.exists()) || file.size === 0) throw new Error(`Missing or empty native artifact: ${path}`);
const hash = new Bun.CryptoHasher("sha256");
let bytes = 0;
for await (const chunk of file.stream()) {
hash.update(chunk);
bytes += chunk.length;
}
if (bytes !== file.size) throw new Error("Native artifact changed during hashing");
return { bytes, sha256: hash.digest("hex") };
}
export async function nativePlan(id: string, flags: Record<string, string>): Promise<NativePlan> {
const catalog: TargetCatalog = await Bun.file(join(import.meta.dir, "native-targets.json")).json();
if (catalog.schemaVersion !== 1 || catalog.jobs !== 2) throw new Error("Invalid native target catalog");
const target = catalog.targets.find(target => target.id === id);
if (!target) throw new Error(`Unknown native target: ${id}`);
const lock = await readLock();
const source = lock.repositories.find(source => source.id === target.sourceId);
if (!source || source.revision !== target.revision)
throw new Error("Native recipe source pin differs from the current source lock");
const configPath = flags.config ? resolve(flags.config) : undefined;
const config: NativeConfig = configPath ? await Bun.file(configPath).json() : {};
const configDirectory = configPath ? dirname(configPath) : packageRoot;
const variables: Record<string, string> = {
source: flags.source ? resolve(flags.source) : "{source}",
out: flags.out ? resolve(flags.out) : "{out}",
revision: target.revision,
kernelConfig: flags["kernel-config"] ? resolve(flags["kernel-config"]) : "{kernelConfig}",
windowsToolchain: config.windowsToolchain
? resolve(configDirectory, config.windowsToolchain)
: "{windowsToolchain}",
baseImage: config.baseImage ?? "{baseImage}",
version: config.version ?? "{version}",
image: `bun-os-wslg:${target.revision.slice(0, 12)}`,
container: `bun-os-wslg-${crypto.randomUUID()}`,
};
for (const external of target.externalSources ?? [])
variables[external.buildArg] = config.externalPins?.[external.path] ?? `{${external.buildArg}}`;
const expand = (value: string) =>
value.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (token, name) => variables[name] ?? token);
const steps = target.steps.map(step => ({
...step,
cmd: step.cmd?.map(expand),
copy: step.copy ? expand(step.copy) : undefined,
to: step.to ? expand(step.to) : undefined,
env: step.env
? Object.fromEntries(Object.entries(step.env).map(([name, value]) => [name, expand(value)]))
: undefined,
}));
return { target, variables, steps, minimumFreeBytes: catalog.minimumFreeBytes };
}
async function prerequisites(plan: NativePlan, flags: Record<string, string>) {
const { target, variables } = plan;
if (process.platform !== target.host || process.arch !== target.architecture)
throw new Error(`Target requires a native ${target.host}/${target.architecture} execution context`);
if (!flags.source || !flags.out) throw new Error("Factory execution requires explicit --source and --out paths");
if (target.portRequired)
throw new Error(`Official APT source needs its native MSVC/UCRT port first:\n${target.requirements.join("\n")}`);
const source = variables.source!;
const out = variables.out!;
await cleanPin(source, target.revision);
for (const file of target.ownerFiles)
if (!(await Bun.file(join(source, file)).exists())) throw new Error(`Missing owner source: ${file}`);
for (const tool of target.tools) if (!Bun.which(tool)) throw new Error(`Missing native prerequisite: ${tool}`);
for (const module of target.pkgConfig) await run(["pkg-config", "--exists", module], source, 15_000);
if (target.id === "linux-aphrody-bun" || target.id === "wsl-kernel") {
if (target.id === "linux-aphrody-bun" && !flags["kernel-config"])
throw new Error("Linux aphrody-bun requires --kernel-config");
const file = Bun.file(
target.id === "wsl-kernel" ? join(source, "arch/x86/configs/aphrody_wsl_defconfig") : variables.kernelConfig!,
);
if (!(await file.exists()) || file.size > 1024 * 1024) throw new Error("Missing or oversized kernel configuration");
const config = await file.text();
if (
!/^CONFIG_RUST=y$/m.test(config) ||
!/^CONFIG_BUN_ACCEL=[ym]$/m.test(config) ||
(target.id === "linux-aphrody-bun" && !/^CONFIG_X86_64=y$/m.test(config)) ||
(target.id === "wsl-kernel" && !/^CONFIG_BUN_ACCEL=y$/m.test(config))
)
throw new Error("Kernel configuration must select Rust and Bun acceleration for the owner architecture");
const sysroot = await run(["rustc", "--print", "sysroot"], source, 15_000);
if (Bun.env.RUST_LIB_SRC && !isAbsolute(Bun.env.RUST_LIB_SRC))
throw new Error("RUST_LIB_SRC must be absolute when configured");
const rustSources = Bun.env.RUST_LIB_SRC ?? join(sysroot, "lib/rustlib/src/rust/library");
if (!(await Bun.file(join(rustSources, "core/src/lib.rs")).exists()))
throw new Error("Kernel Rust requires matching rust-src or an explicit RUST_LIB_SRC");
}
if (target.id === "wsl-windows") {
if (await Bun.file(join(source, "UserConfig.cmake")).exists())
throw new Error("UserConfig.cmake may enable automatic deployment; select a clean build source without it");
await run(
[
process.execPath,
"msvc",
"--instance",
"2022",
"--toolset",
"v143",
"--sdk",
"10.0.26100.0",
"--arch",
"x64",
"doctor",
],
source,
30_000,
);
const version = await run(["cmake", "--version"], source, 15_000);
const match = /cmake version (\d+)\.(\d+)/.exec(version);
if (!match || Number(match[1]) < 3 || (Number(match[1]) === 3 && Number(match[2]) < 25))
throw new Error("WSL requires CMake 3.25 or newer");
}
if (target.id === "apt-gnu") {
if (!Bun.which("getconf")) throw new Error("GNU APT needs a glibc host and getconf");
const libc = await run(["getconf", "GNU_LIBC_VERSION"], source, 15_000);
if (!/^glibc \d+\.\d+/.test(libc)) throw new Error("APT GNU recipe requires glibc");
await run(["perl", "-MDpkg", "-e", "print $Dpkg::DATADIR"], source, 15_000);
for (const library of ["libdb.so", "libbz2.so"]) {
const path = await run(["g++", `-print-file-name=${library}`], source, 15_000);
if (!isAbsolute(path) || !(await Bun.file(path).exists())) throw new Error(`Missing GNU APT library: ${library}`);
}
}
if (target.id === "wslg-system") {
if (!/@sha256:[a-f0-9]{64}$/.test(variables.baseImage!) || /[{}]/.test(variables.baseImage!))
throw new Error("WSLg requires an immutable baseImage digest in --config");
if (!/^[0-9][0-9A-Za-z.+-]*$/.test(variables.version!))
throw new Error("WSLg requires an explicit version in --config");
for (const external of target.externalSources!)
await cleanPin(join(source, external.path), variables[external.buildArg]!);
const system = await run(["docker", "info", "--format", "{{.OSType}}"], source, 30_000);
if (system !== "linux") throw new Error("WSLg requires a live Linux Docker daemon on this factory");
}
try {
await lstat(out);
throw new Error("Native output directory already exists; refusing replacement");
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
let parent = dirname(out);
for (;;) {
try {
await lstat(parent);
break;
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
const next = dirname(parent);
if (next === parent) throw error;
parent = next;
}
}
const filesystem = await statfs(parent, { bigint: true });
const canonicalOut = join(await realpath(parent), relative(parent, out));
const rel = relative(await realpath(source), canonicalOut);
if (!rel || (!isAbsolute(rel) && rel !== ".." && !rel.startsWith(`..${sep}`)))
throw new Error("Build output must be outside the source checkout, including resolved links");
const free = filesystem.bavail * filesystem.bsize;
if (free < BigInt(plan.minimumFreeBytes)) throw new Error("Native factory requires at least 50 GiB free");
return { freeBytes: free.toString(), sourcePinVerified: true, runtimeQualified: false };
}
export async function nativeBuild(plan: NativePlan, flags: Record<string, string>) {
const state = await prerequisites(plan, flags);
const out = plan.variables.out!;
const factory = flags.factory ? resolve(flags.factory) : dirname(out);
const factoryRel = relative(factory, out);
if (!factoryRel || factoryRel === ".." || factoryRel.startsWith(`..${sep}`) || isAbsolute(factoryRel))
throw new Error("Output must be inside its selected factory directory");
if (plan.target.host === "linux") {
const profile: { sharedHeavyLockRelative: string } = await Bun.file(join(import.meta.dir, "profile.json")).json();
if (!profile.sharedHeavyLockRelative || isAbsolute(profile.sharedHeavyLockRelative))
throw new Error("Missing relative shared Linux factory lock");
const heavyLock = resolve(factory, profile.sharedHeavyLockRelative);
if (!Bun.which("flock"))
throw new Error("Linux native builds require flock to coordinate the existing heavy owner");
if (!(await lstat(dirname(heavyLock))).isDirectory())
throw new Error("Shared heavy lock directory must already be provisioned by the factory owner");
if (Bun.env.BUN_OS_NATIVE_LOCK_HELD !== heavyLock) {
const args = Object.entries(flags).flatMap(([name, value]) => [`--${name}`, value]);
const timeout = plan.steps.reduce((total, step) => total + (step.timeoutMs ?? 0), 300_000);
const output = await run(
["flock", "-n", heavyLock, process.execPath, import.meta.path, "build", plan.target.id, ...args],
packageRoot,
timeout,
undefined,
{ BUN_OS_NATIVE_LOCK_HELD: heavyLock },
);
return JSON.parse(output);
}
}
await mkdir(factory, { recursive: true });
const lockPath = join(factory, ".factory.lock");
const lock = await open(lockPath, "wx");
const token = JSON.stringify({ pid: process.pid, target: plan.target.id, out, token: crypto.randomUUID() });
try {
await lock.writeFile(token + "\n");
await mkdir(out);
await mkdir(join(out, "logs"));
await mkdir(join(out, "build"));
if (plan.target.id === "wsl-kernel") await mkdir(join(out, "perf-build"));
await Bun.write(join(out, "plan.json"), JSON.stringify({ ...plan, state }, null, 2) + "\n");
const variables = { ...plan.variables };
for (let index = 0; index < plan.steps.length; index++) {
const step = plan.steps[index]!;
const expand = (value: string) =>
value.replace(/\{([A-Za-z_][A-Za-z0-9_]*)\}/g, (token, name) => variables[name] ?? token);
if (step.copy && step.to) {
const destination = contained(out, expand(step.to));
const input = Bun.file(expand(step.copy));
if (!(await input.exists()) || input.size > 1024 * 1024)
throw new Error("Missing or oversized configuration input");
const file = await open(destination, "wx");
try {
await file.writeFile(new Uint8Array(await input.arrayBuffer()));
} finally {
await file.close();
}
} else if (step.cmd && step.timeoutMs) {
const cmd = step.cmd.map(expand);
if (cmd.some(arg => /\{[A-Za-z_][A-Za-z0-9_]*\}/.test(arg)))
throw new Error("Native build command has unresolved inputs");
const env = step.env
? Object.fromEntries(Object.entries(step.env).map(([name, value]) => [name, expand(value)]))
: undefined;
const output = await run(
cmd,
out,
step.timeoutMs,
step.capture ? undefined : join(out, "logs", `${index + 1}.log`),
env,
);
if (step.capture) {
if (!/^[A-Za-z0-9._+-]+$/.test(output)) throw new Error("Unexpected captured kernel release");
variables[step.capture] = output;
}
if (step.checkKernelConfig) {
const config = await Bun.file(join(out, "build/.config")).text();
if (
!/^CONFIG_X86_64=y$/m.test(config) ||
!/^CONFIG_RUST=y$/m.test(config) ||
!/^CONFIG_BUN_ACCEL=[ym]$/m.test(config) ||
(plan.target.id === "wsl-kernel" && !/^CONFIG_BUN_ACCEL=y$/m.test(config))
)
throw new Error(
"Resolved kernel configuration dropped its required architecture, Rust or Bun acceleration",
);
}
} else throw new Error("Invalid native build step");
}
const artifacts = [];
for (const path of plan.target.artifacts) {
const artifact = contained(out, path);
const identity = await digest(artifact);
const pe = path.endsWith("wsl.exe");
if (pe && identity.bytes > 256 * 1024 * 1024)
throw new Error("Native PE artifact exceeds the bounded inspection size");
const bytes = pe
? new Uint8Array(await Bun.file(artifact).arrayBuffer())
: new Uint8Array(await Bun.file(artifact).slice(0, 64).arrayBuffer());
const inspected = pe || path.endsWith("vmlinux") || path.startsWith("stage/usr/bin/") ? abi(bytes) : undefined;
if (
pe &&
(inspected?.format !== "pe" ||
inspected.architecture !== "x64" ||
inspected.runtime !== "windows" ||
!inspected.executable)
)
throw new Error("WSL artifact is not the expected native Windows PE x64 executable");
if (
(path.endsWith("vmlinux") || path.startsWith("stage/usr/bin/")) &&
(inspected?.format !== "elf" || inspected.architecture !== "x64")
)
throw new Error("GNU/kernel artifact is not the expected ELF x64 file");
artifacts.push({ path, ...identity, abi: inspected });
}
const receipt = {
schemaVersion: 1,
target: plan.target.id,
revision: plan.target.revision,
artifactAbi: plan.target.abi,
artifacts,
sourcePinVerified: true,
runtimeQualified: false,
unqualified: plan.target.unqualified,
};
await Bun.write(join(out, "receipt.json"), JSON.stringify(receipt, null, 2) + "\n");
return receipt;
} finally {
await lock.close();
const file = Bun.file(lockPath);
if ((await file.exists()) && (await file.text()).trim() === token) await unlink(lockPath);
}
}
async function main() {
const args = Bun.argv.slice(2);
const action = args.shift();
if (!action || action === "--help" || action === "help") {
console.log(usage);
return;
}
const id = args.shift();
if (!id || !["plan", "status", "build"].includes(action)) throw new Error(usage);
const flags: Record<string, string> = Object.create(null);
while (args.length) {
const flag = args.shift()!;
const value = args.shift();
const name = flag.slice(2);
if (
!flag.startsWith("--") ||
!["source", "out", "config", "kernel-config", "factory"].includes(name) ||
!value ||
value.startsWith("--") ||
flags[name]
)
throw new Error(`Invalid native factory option ${flag}`);
flags[name] = value;
}
const plan = await nativePlan(id, flags);
const result =
action === "plan" ? plan : action === "status" ? await prerequisites(plan, flags) : await nativeBuild(plan, flags);
console.log(JSON.stringify(result, null, 2));
}
if (import.meta.main)
main().catch(error => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
+58
View File
@@ -0,0 +1,58 @@
{
"schemaVersion": 1,
"host": "vps",
"distribution": "ubuntu",
"series": "resolute",
"architecture": "amd64",
"abi": "gnu",
"libc": "glibc",
"minimumFreeBytes": 53687091200,
"sourcePackages": ["apt", "dpkg", "livecd-rootfs", "ubuntu-meta", "linux"],
"sourceComponents": ["main"],
"sourcePockets": ["resolute", "resolute-updates", "resolute-security"],
"packageBuilder": "sbuild",
"rootfsBuilder": "live-build",
"rootfsOverlay": "livecd-rootfs",
"imageAssembler": "debian-cd",
"dependencyResolver": "germinate",
"sourceArchive": "https://archive.ubuntu.com/ubuntu",
"securityArchive": "https://security.ubuntu.com/ubuntu",
"keyring": "/usr/share/keyrings/ubuntu-archive-keyring.gpg",
"jobDeadlineSeconds": 1200,
"commandTimeoutSeconds": 180,
"ubuntuImage": "ubuntu:26.04",
"engineLifetimeSeconds": 21600,
"toolchainTimeoutSeconds": 1800,
"sharedHeavyLockRelative": "../locks/heavy.lock",
"toolchainPackages": [
"ca-certificates",
"ubuntu-keyring",
"apt",
"dpkg-dev",
"sbuild",
"schroot",
"debootstrap",
"live-build",
"livecd-rootfs",
"germinate",
"debian-cd",
"xorriso",
"squashfs-tools",
"mtools",
"dosfstools",
"qemu-utils",
"zstd",
"fakeroot",
"mmdebstrap",
"uidmap"
],
"references": [
"https://ubuntu.com/project/docs/how-ubuntu-is-made/processes/image-builds/",
"https://ubuntu.com/project/docs/contributors/building/build-packages-locally",
"https://documentation.ubuntu.com/project/contributors/setup/get-the-source-of-a-package/",
"https://documentation.ubuntu.com/project/contributors/setup/set-up-for-ubuntu-development/",
"https://docs.docker.com/reference/cli/dockerd/#run-multiple-daemons"
],
"packageBackend": "unshare",
"packageUser": "ubuntu"
}
File diff suppressed because it is too large. Load diff
+976
View File
@@ -0,0 +1,976 @@
#!/usr/bin/env bun
import { chmod, mkdir, open, realpath, stat, statfs, unlink } from "node:fs/promises";
import { isAbsolute, join, relative, resolve } from "node:path";
interface FactoryProfile {
schemaVersion: 1;
series: string;
architecture: string;
minimumFreeBytes: number;
sourcePackages: string[];
sourceArchive: string;
securityArchive: string;
keyring: string;
sourcePockets: string[];
sourceComponents: string[];
jobDeadlineSeconds: number;
ubuntuImage: string;
toolchainPackages: string[];
packageUser: string;
engineLifetimeSeconds: number;
sharedHeavyLockRelative: string;
}
interface ArchiveProof {
path: string;
bytes: number;
sha256: string;
name: string;
url: string;
expectedSha256: string;
verified: boolean;
}
interface SourceProof {
name: string;
version: string;
sourceDirectory: string;
dsc: ArchiveProof | undefined;
archives: ArchiveProof[];
authentication: string;
extraction: {
tool: string;
strongChecksumsRequired: boolean;
defaultSignatureChecksPreserved: boolean;
exitCode: number;
signatureMessages: string[];
};
}
const usage =
"Ubuntu Bun OS factory (native/SSH/Docker Linux; no workstation WSL)\n" +
" status --root ABSOLUTE_DIRECTORY\n sources [PACKAGE...] --root ABSOLUTE_DIRECTORY\n" +
" engine --root ABSOLUTE_DIRECTORY\n toolchain --root ABSOLUTE_DIRECTORY\n" +
" package FILE.dsc --root ABSOLUTE_DIRECTORY [--chroot CROSS_BUILD_NAME]\n" +
" rootfs --root ABSOLUTE_DIRECTORY --config OWNED_LIVE_BUILD_DIRECTORY\n";
function parse(values: string[]) {
const flags: Record<string, string> = {},
positional: string[] = [];
for (let i = 0; i < values.length; i++) {
const arg = values[i]!;
if (!arg.startsWith("--")) {
positional.push(arg);
continue;
}
const key = arg.slice(2),
value = values[++i];
if (!["root", "chroot", "config"].includes(key) || !value || value.startsWith("--") || flags[key])
throw new Error("Invalid factory option " + arg);
flags[key] = value;
}
return { flags, positional };
}
function owned(root: string, path: string) {
const suffix = relative(root, path);
if (!suffix || suffix === ".." || suffix.startsWith("../") || isAbsolute(suffix))
throw new Error("Expected a path inside the selected factory root");
return path;
}
async function sameDevice(root: string, path: string) {
if ((await stat(root)).dev !== (await stat(path)).dev)
throw new Error("Factory output must stay on its build filesystem");
}
async function command(args: string[], job: string, step: string, timeout = 120000) {
const stdoutPath = join(job, "logs", step + ".stdout"),
stderrPath = join(job, "logs", step + ".stderr");
const [output, error] = await Promise.all([open(stdoutPath, "w"), open(stderrPath, "w")]);
let code: number;
try {
const child = Bun.spawn({
cmd: args,
cwd: job,
env: {
...Bun.env,
TMPDIR: join(job, "tmp"),
TMP: join(job, "tmp"),
TEMP: join(job, "tmp"),
XDG_CACHE_HOME: join(job, "cache"),
DOCKER_CONFIG: join(job, "docker-config"),
},
stdin: "ignore",
stdout: output.fd,
stderr: error.fd,
timeout,
});
await Bun.write(
join(job, "status.json"),
JSON.stringify(
{ schemaVersion: 1, status: "running", step, pid: process.pid, activeChildPid: child.pid, jobPath: job },
null,
2,
) + "\n",
);
code = await child.exited;
} finally {
await Promise.all([output.close(), error.close()]);
}
if (code !== 0) throw new Error(step + " failed exit=" + code + "; logs=" + job + "/logs/" + step);
return { stdout: await Bun.file(stdoutPath).text(), stderr: await Bun.file(stderrPath).text() };
}
async function artifactProof(root: string, path: string) {
const resolved = owned(root, await realpath(path));
await sameDevice(root, resolved);
const file = Bun.file(resolved),
hasher = new Bun.CryptoHasher("sha256");
for await (const chunk of file.stream()) hasher.update(chunk);
return { path: resolved, bytes: (await stat(resolved)).size, sha256: hasher.digest("hex") };
}
function perlString(value: string) {
return "'" + value.replaceAll("\\", "\\\\").replaceAll("'", "\\'") + "'";
}
async function configureSources(job: string, profile: FactoryProfile) {
for (const path of [
"etc/parts",
"etc/sourceparts",
"etc/preferencesparts",
"etc/netrcparts",
"etc/trustedparts",
"state/lists/partial",
"cache/archives/partial",
"downloads",
"sources",
"gnupg",
])
await mkdir(join(job, path), { recursive: true });
await Bun.write(join(job, "state/status"), "");
const sources =
"Types: deb-src\nURIs: " +
profile.sourceArchive +
"\nSuites: " +
profile.sourcePockets.filter(p => !p.endsWith("-security")).join(" ") +
"\nComponents: " +
profile.sourceComponents.join(" ") +
"\nSigned-By: " +
profile.keyring +
"\n\n" +
"Types: deb-src\nURIs: " +
profile.securityArchive +
"\nSuites: " +
profile.series +
"-security\nComponents: " +
profile.sourceComponents.join(" ") +
"\nSigned-By: " +
profile.keyring +
"\n";
await Bun.write(join(job, "etc/ubuntu.sources"), sources);
const config: Record<string, string> = {
"Dir::Etc::main": "/dev/null",
"Dir::Etc::parts": join(job, "etc/parts"),
"Dir::Etc::sourcelist": join(job, "etc/ubuntu.sources"),
"Dir::Etc::sourceparts": join(job, "etc/sourceparts"),
"Dir::Etc::preferences": "/dev/null",
"Dir::Etc::preferencesparts": join(job, "etc/preferencesparts"),
"Dir::Etc::netrc": "/dev/null",
"Dir::Etc::netrcparts": join(job, "etc/netrcparts"),
"Dir::Etc::trusted": profile.keyring,
"Dir::Etc::trustedparts": join(job, "etc/trustedparts"),
"Dir::State": join(job, "state"),
"Dir::State::status": join(job, "state/status"),
"Dir::State::lists": join(job, "state/lists"),
"Dir::State::extended_states": join(job, "state/extended_states"),
"Dir::Cache": join(job, "cache"),
"Dir::Cache::archives": join(job, "cache/archives"),
"Dir::Cache::srcpkgcache": "",
"Dir::Cache::pkgcache": "",
"Dir::Log": join(job, "logs"),
"APT::Get::AllowUnauthenticated": "false",
"Acquire::AllowInsecureRepositories": "false",
"Acquire::AllowDowngradeToInsecureRepositories": "false",
"Acquire::Check-Valid-Until": "true",
"Acquire::https::Verify-Peer": "true",
"Acquire::https::Verify-Host": "true",
"Acquire::https::Timeout": "30",
"Acquire::Retries": "1",
"APT::Get::List-Cleanup": "false",
};
await Bun.write(
join(job, "apt.conf"),
Object.entries(config)
.map(([key, value]) => key + " " + JSON.stringify(value) + ";")
.join("\n") + "\n",
);
}
async function fetchSources(job: string, root: string, profile: FactoryProfile, names: string[]) {
const keyring = profile.keyring;
const startedAt = new Date().toISOString();
const deadline = Date.now() + profile.jobDeadlineSeconds * 1000;
const env = {
...Bun.env,
APT_CONFIG: job + "/apt.conf",
TMPDIR: job + "/tmp",
TMP: job + "/tmp",
TEMP: job + "/tmp",
XDG_CACHE_HOME: job + "/cache",
GNUPGHOME: job + "/gnupg",
BUN_RUNTIME_TRANSPILER_CACHE_PATH: job + "/cache/transpiler",
};
Bun.env.TMPDIR = env.TMPDIR;
const state: Record<string, unknown> = {
schemaVersion: 1,
host: "vps",
root,
jobPath: job,
pid: process.pid,
startedAt,
status: "running",
step: "startup",
activeChildPid: null,
};
async function status(step: string, extra: Record<string, unknown> = {}) {
Object.assign(state, { step, ...extra });
await Bun.write(job + "/status.json", JSON.stringify(state, null, 2) + "\n");
console.log(new Date().toISOString() + " " + step);
}
async function run(
args: string[],
step: string,
{ cwd = job, limit = 120000 }: { cwd?: string; limit?: number } = {},
) {
await status(step);
const remaining = deadline - Date.now();
if (remaining <= 0) throw new Error("source job deadline reached");
const p = Bun.spawn(args, { cwd, env, stdout: "pipe", stderr: "pipe", timeout: Math.min(limit, remaining) });
await status(step, { activeChildPid: p.pid });
const [stdout, stderr, code] = await Promise.all([
new Response(p.stdout).text(),
new Response(p.stderr).text(),
p.exited,
]);
await Bun.write(job + "/logs/" + step + ".stdout", stdout);
await Bun.write(job + "/logs/" + step + ".stderr", stderr);
await status(step, { activeChildPid: null, exitCode: code });
if (code !== 0) throw new Error(step + " failed exit=" + code + "; logs=" + job + "/logs/" + step);
return { stdout, stderr, code };
}
async function hash(path: string) {
const f = Bun.file(path);
const hasher = new Bun.CryptoHasher("sha256");
for await (const chunk of f.stream()) hasher.update(chunk);
return { path, bytes: (await stat(path)).size, sha256: hasher.digest("hex") };
}
function field(text: string, name: string) {
return text.match(new RegExp("^" + name + ":\\s*(.*)$", "m"))?.[1]?.trim();
}
function sourceHashes(text: string) {
const lines = text.split(/\r?\n/);
const start = lines.findIndex(line => line === "Checksums-Sha256:");
if (start < 0) throw new Error("signed source metadata has no SHA256 checksums");
const result: { sha256: string; bytes: number; name: string }[] = [];
for (let i = start + 1; i < lines.length && /^\s/.test(lines[i]); i++) {
const m = lines[i].match(/^\s+([a-f0-9]{64})\s+(\d+)\s+(\S+)\s*$/);
if (m) result.push({ sha256: m[1], bytes: Number(m[2]), name: m[3] });
}
if (!result.length) throw new Error("empty strong source checksum set");
return result;
}
try {
const osText = await Bun.file("/etc/os-release").text();
const os = {
id: osText.match(/^ID=(.*)$/m)?.[1]?.replaceAll('"', ""),
versionId: osText.match(/^VERSION_ID=(.*)$/m)?.[1]?.replaceAll('"', ""),
codename: osText.match(/^VERSION_CODENAME=(.*)$/m)?.[1]?.replaceAll('"', ""),
architecture: "amd64",
};
if (os.id !== "ubuntu" || os.codename !== profile.series) throw new Error("unexpected host distribution");
await run(["apt-get", "-o", "APT::Update::Error-Mode=any", "update"], "apt-update", { limit: 300000 });
const releases = [];
for await (const name of new Bun.Glob("*_InRelease").scan({ cwd: job + "/state/lists", onlyFiles: true })) {
const path = job + "/state/lists/" + name;
const verified = await run(
["gpgv", "--status-fd", "1", "--keyring", keyring, path],
"verify-release-" + releases.length,
{ limit: 30000 },
);
const fingerprints = [...verified.stdout.matchAll(/^\[GNUPG:\] VALIDSIG ([a-f0-9]+)/gim)].map(match => match[1]);
if (!fingerprints.length) throw new Error("no valid Ubuntu archive signature " + name);
releases.push({ ...(await hash(path)), signatureVerified: true, signerFingerprints: fingerprints });
}
if (releases.length !== profile.sourcePockets.length)
throw new Error("expected all authenticated Ubuntu pockets, got " + releases.length);
const sources: SourceProof[] = [];
for (const pkg of names) {
const uriOutput = await run(
["apt-get", "--print-uris", "--download-only", "--only-source", "source", pkg],
"uris-" + pkg,
{ cwd: job + "/downloads", limit: 30000 },
);
const uris = new Map(
[...uriOutput.stdout.matchAll(/^'([^']+)'\s+(\S+)\s+(\d+)\s+(.*)$/gm)].map(m => [
m[2],
{ url: m[1], bytes: Number(m[3]), aptHash: m[4] },
]),
);
await run(["apt-get", "--download-only", "--only-source", "source", pkg], "download-" + pkg, {
cwd: job + "/downloads",
limit: 180000,
});
const names = [];
for await (const name of new Bun.Glob(pkg + "_*.dsc").scan({ cwd: job + "/downloads", onlyFiles: true }))
names.push(name);
if (names.length !== 1) throw new Error("expected one DSC for " + pkg);
const dscName = names[0],
dscPath = job + "/downloads/" + dscName,
dscText = await Bun.file(dscPath).text();
const version = field(dscText, "Version"),
sourceName = field(dscText, "Source");
if (sourceName !== pkg || !version) throw new Error("DSC identity mismatch " + pkg);
const meta = await run(["apt-cache", "--only-source", "showsrc", pkg], "metadata-" + pkg, { limit: 30000 });
const record = meta.stdout
.split(/\n\s*\n/)
.find(record => field(record, "Package") === pkg && field(record, "Version") === version);
if (!record) throw new Error("authenticated source record not found " + pkg + "=" + version);
const files: ArchiveProof[] = [];
for (const expected of sourceHashes(record)) {
if (expected.name.includes("/") || expected.name.includes("\\"))
throw new Error("source archive filename escapes downloads");
const path = job + "/downloads/" + expected.name;
const actual = await hash(path);
if (actual.sha256 !== expected.sha256 || actual.bytes !== expected.bytes)
throw new Error("source archive hash/size mismatch " + expected.name);
const uri = uris.get(expected.name);
if (!uri || !uri.url.startsWith("https://"))
throw new Error("missing authenticated HTTPS source URI " + expected.name);
files.push({ ...actual, name: expected.name, url: uri.url, expectedSha256: expected.sha256, verified: true });
}
const sourceDirectory = job + "/sources/" + pkg;
const extraction = await run(
[
"dpkg-source",
"--require-strong-checksums",
"--no-overwrite-dir",
"--no-copy",
"--threads-max=1",
"--extract",
dscPath,
sourceDirectory,
],
"extract-" + pkg,
{ limit: 120000 },
);
if (!(await Bun.file(sourceDirectory + "/debian/changelog").exists()))
throw new Error("missing extracted Debian source metadata " + pkg);
sources.push({
name: pkg,
version,
sourceDirectory,
dsc: files.find(file => file.name === dscName),
archives: files.filter(file => file.name !== dscName),
authentication: "Ubuntu signed InRelease -> authenticated Sources SHA256 -> verified DSC and archive SHA256",
extraction: {
tool: "dpkg-source",
strongChecksumsRequired: true,
defaultSignatureChecksPreserved: true,
exitCode: 0,
signatureMessages: extraction.stderr
.split(/\r?\n/)
.filter(line => /signature|key|gpg/i.test(line))
.slice(0, 12),
},
});
await status("source-ready-" + pkg, {
packagesReady: sources.map(source => ({
name: source.name,
version: source.version,
sourceDirectory: source.sourceDirectory,
})),
});
}
const receipt = {
schemaVersion: 1,
status: "complete",
host: "vps",
os,
root,
job: {
path: job,
pid: process.pid,
startedAt,
completedAt: new Date().toISOString(),
deadlineSeconds: profile.jobDeadlineSeconds,
},
executor: {
bunVersion: Bun.version,
bunRevision: Bun.revision,
apt: (await run(["apt-get", "--version"], "apt-version", { limit: 10000 })).stdout.split("\n")[0],
dpkgSource: (await run(["dpkg-source", "--version"], "dpkg-source-version", { limit: 10000 })).stdout.split(
"\n",
)[0],
},
archive: {
repositories: [profile.sourceArchive, profile.securityArchive],
suites: profile.sourcePockets,
components: profile.sourceComponents,
signedBy: keyring,
keyring: await hash(keyring),
inRelease: releases,
},
packages: sources,
scope: {
operation: "source-retrieval-and-extraction",
builds: false,
installs: false,
globalAptStateModified: false,
temporaryRoot: env.TMPDIR,
},
guidance: [
"https://documentation.ubuntu.com/project/contributors/setup/get-the-source-of-a-package/",
"https://documentation.ubuntu.com/project/contributors/setup/set-up-for-ubuntu-development/",
],
};
await Bun.write(job + "/receipt.json", JSON.stringify(receipt, null, 2) + "\n");
await status("complete", { status: "complete", exitCode: 0, receiptPath: job + "/receipt.json" });
return receipt;
} catch (error) {
await status("failed", { status: "failed", exitCode: 1, error: String(error) });
throw error;
}
}
function alive(pid: number) {
try {
process.kill(pid, 0);
return true;
} catch (error) {
return (error as NodeJS.ErrnoException).code === "EPERM";
}
}
async function engineRecord(root: string) {
const directory = join(root, "engine"),
file = Bun.file(join(directory, "launcher.pid"));
if (!(await file.exists())) throw new Error("No factory Docker engine launcher");
const pid = Number((await file.text()).trim());
if (!Number.isSafeInteger(pid) || pid < 1 || !alive(pid))
throw new Error("Factory Docker engine launcher is not live");
return {
root,
directory,
pid,
socket: "unix://" + join(directory, "docker.sock"),
dataRoot: join(directory, "data"),
};
}
async function launchEngine(root: string, job: string, profile: FactoryProfile) {
const directory = join(root, "engine");
for (const path of ["data", "run", "tmp", "logs", "cache"]) await mkdir(join(directory, path), { recursive: true });
await sameDevice(root, directory);
if (await Bun.file(join(directory, "launcher.pid")).exists()) {
const previous = await engineRecord(root);
return { schemaVersion: 1, kind: "factory-engine-existing", ...previous };
}
await command(["sudo", "-n", "true"], job, "engine-permission", 10000);
await Bun.write(join(directory, "daemon.json"), "{}\n");
const log = await open(join(directory, "logs/daemon.log"), "a");
const child = Bun.spawn({
cmd: [
"nohup",
"sudo",
"-n",
"env",
"TMPDIR=" + join(directory, "tmp"),
"TMP=" + join(directory, "tmp"),
"TEMP=" + join(directory, "tmp"),
"XDG_CACHE_HOME=" + join(directory, "cache"),
"timeout",
"--kill-after=15",
String(profile.engineLifetimeSeconds),
"dockerd",
"--config-file=" + join(directory, "daemon.json"),
"--data-root=" + join(directory, "data"),
"--exec-root=" + join(directory, "run"),
"--pidfile=" + join(directory, "dockerd.pid"),
"--host=unix://" + join(directory, "docker.sock"),
"--group=docker",
"--bridge=none",
"--iptables=false",
"--ip6tables=false",
"--ip-masq=false",
"--ip-forward=false",
"--userland-proxy=false",
"--containerd-namespace=bun-os-factory",
"--containerd-plugins-namespace=bun-os-factory-plugins",
],
cwd: job,
stdin: "ignore",
stdout: log.fd,
stderr: log.fd,
});
child.unref();
await Bun.write(join(directory, "launcher.pid"), String(child.pid) + "\n");
await log.close();
return {
schemaVersion: 1,
kind: "factory-engine-start-requested",
root,
pid: child.pid,
socket: "unix://" + join(directory, "docker.sock"),
dataRoot: join(directory, "data"),
lifetimeSeconds: profile.engineLifetimeSeconds,
globalDaemonModified: false,
};
}
async function dockerOwner(root: string, job: string) {
const engine = await engineRecord(root);
const args = ["docker", "--host", engine.socket];
const result = await command([...args, "info", "--format", "{{json .}}"], job, "docker-info", 30000);
const info = JSON.parse(result.stdout) as { DockerRootDir: string; OSType: string; Architecture: string };
const data = await realpath(info.DockerRootDir);
if (info.OSType !== "linux" || data !== (await realpath(engine.dataRoot)))
throw new Error("Docker endpoint does not own the factory Linux data root");
owned(root, data);
await sameDevice(root, data);
return { engine, args, info };
}
async function provisionTools(root: string, job: string, profile: FactoryProfile) {
const owner = await dockerOwner(root, job);
if (!/^ubuntu:(\d+\.\d+)$/.test(profile.ubuntuImage) && !/^ubuntu@sha256:[a-f0-9]{64}$/.test(profile.ubuntuImage))
throw new Error("Expected the official Ubuntu OCI image");
await command(
[...owner.args, "pull", "--platform", "linux/" + profile.architecture, profile.ubuntuImage],
job,
"ubuntu-image-pull",
180000,
);
const base = JSON.parse(
(await command([...owner.args, "image", "inspect", profile.ubuntuImage], job, "ubuntu-image-inspect", 30000))
.stdout,
) as { Id: string; RepoDigests: string[] }[];
const digest = base[0]?.RepoDigests.find(value => /^ubuntu@sha256:[a-f0-9]{64}$/.test(value));
if (!digest) throw new Error("Ubuntu base has no immutable registry digest");
const recipe =
"FROM " +
digest +
"\nENV DEBIAN_FRONTEND=noninteractive\nRUN apt-get update && " +
"apt-get install -y --no-install-recommends " +
profile.toolchainPackages.join(" ") +
"\n" +
"RUN dpkg-query -W -f='" +
"$" +
"{binary:Package}\\t" +
"$" +
"{Version}\\n' > /usr/share/bun-os-factory-packages.txt\n";
const dockerfile = join(job, "Dockerfile");
await Bun.write(dockerfile, recipe);
const tag = "bun-os-factory:" + profile.series + "-" + profile.architecture;
await command(
[...owner.args, "build", "--network=host", "--progress=plain", "--tag", tag, "--file", dockerfile, job],
job,
"toolchain-build",
1800000,
);
const images = JSON.parse(
(await command([...owner.args, "image", "inspect", tag], job, "toolchain-image-inspect", 30000)).stdout,
) as { Id: string; Size: number }[];
const image = images[0];
if (!image?.Id.startsWith("sha256:")) throw new Error("Toolchain image identity unavailable");
const tools = await command(
[
...owner.args,
"run",
"--rm",
"--network=none",
"--cpus=2",
"--memory=2g",
image.Id,
"bash",
"-eu",
"-c",
"sbuild --version; lb --version; germinate --version; " +
"dpkg-query -W apt dpkg-dev sbuild mmdebstrap uidmap schroot debootstrap live-build livecd-rootfs germinate debian-cd; " +
"cat /etc/os-release; ldd --version",
],
job,
"toolchain-tools",
30000,
);
const receipt = {
schemaVersion: 1,
kind: "ubuntu-toolchain",
status: "complete",
root,
series: profile.series,
architecture: profile.architecture,
image: { tag, id: image.Id, bytes: image.Size, base: digest },
engine: owner.engine,
tools: tools.stdout,
qualification: { sourceBuilds: false, cleanSbuildChroot: false, rootfsBuilt: false, osImageBuilt: false },
};
await Bun.write(join(root, "toolchain.json"), JSON.stringify(receipt, null, 2) + "\n");
return receipt;
}
async function buildContainer(
root: string,
job: string,
profile: FactoryProfile,
action: string,
positional: string[],
flags: Record<string, string>,
) {
const owner = await dockerOwner(root, job);
const receipt = (await Bun.file(join(root, "toolchain.json")).json()) as { status: string; image: { id: string } };
if (receipt.status !== "complete") throw new Error("Qualified factory toolchain image required");
const common = [
...owner.args,
"run",
"--rm",
"--network=host",
"--cpus=2",
"--memory=4g",
"--pids-limit=1024",
"--cap-add=SYS_ADMIN",
"--security-opt=seccomp=unconfined",
"--security-opt=apparmor=unconfined",
...(action === "package" ? [] : ["--mount", "type=bind,source=" + root + ",target=/factory"]),
"--env",
action === "package" ? "TMPDIR=/job/tmp" : "TMPDIR=/factory/" + relative(root, join(job, "tmp")),
];
if (action === "package") {
if (
positional.length !== 1 ||
!positional[0]!.endsWith(".dsc") ||
(flags.chroot && !/^[a-z0-9][a-z0-9._-]+$/.test(flags.chroot))
)
throw new Error("Expected owned DSC and optional provisioned cross-build schroot");
const source = owned(root, await realpath(resolve(positional[0]!)));
const artifacts = join(job, "artifacts"),
home = join(job, "home"),
cache = join(job, "cache");
const containerPath = (path: string) => "/job/" + relative(job, path);
await chmod(job, 0o755);
await mkdir(artifacts);
await mkdir(join(home, ".config/sbuild"), { recursive: true });
await chmod(join(job, "tmp"), 0o1777);
const config = [
"$chroot_mode = 'unshare';",
"$unshare_mmdebstrap_keep_tarball = 1;",
"$unshare_tmpdir_template = " + perlString(containerPath(join(job, "tmp/tmp.sbuild.XXXXXXXXXX"))) + ";",
"$clean_source = 0;",
"$run_lintian = 0;",
"$log_dir = " + perlString(containerPath(join(job, "logs"))) + ";",
"$unshare_mmdebstrap_extra_args = ['*', [" +
[
"--include=ca-certificates,debhelper,ubuntu-keyring",
"--keyring=" + profile.keyring,
"--components=main,universe",
...profile.sourcePockets.map(
pocket =>
"deb [signed-by=" +
profile.keyring +
"] " +
(pocket.endsWith("-security") ? profile.securityArchive : profile.sourceArchive) +
" " +
pocket +
" main universe",
),
]
.map(perlString)
.join(", ") +
"]];",
"1;",
"",
].join("\n");
await Bun.write(join(home, ".config/sbuild/config.pl"), config);
const input = await artifactProof(root, source);
const dscText = await Bun.file(source).text(),
sourceName = source.split("/").at(-1)!;
const checksumBlock = dscText.match(/^Checksums-Sha256:\r?\n((?:[ \t].*\r?\n)+)/m)?.[1];
if (!checksumBlock) throw new Error("Source descriptor requires SHA256 archive checksums");
const sourceFiles = [{ ...input, name: sourceName }];
for (const line of checksumBlock.trim().split(/\r?\n/)) {
const record = line.trim().match(/^([a-f0-9]{64})\s+(\d+)\s+([a-zA-Z0-9][a-zA-Z0-9.+:_~-]*)$/);
if (!record) throw new Error("Invalid source archive checksum record");
const proof = await artifactProof(root, source.slice(0, source.lastIndexOf("/") + 1) + record[3]);
if (proof.sha256 !== record[1] || proof.bytes !== Number(record[2]))
throw new Error("Source descriptor archive checksum mismatch " + record[3]);
sourceFiles.push({ ...proof, name: record[3]! });
}
const sourceMounts = sourceFiles.flatMap(file => [
"--mount",
"type=bind,source=" + file.path + ",target=/source/" + file.name + ",readonly",
]);
const backend = flags.chroot ? ["--chroot-mode=schroot", "--chroot=" + flags.chroot] : ["--chroot-mode=unshare"];
await command(
[
...common,
"--user",
profile.packageUser,
"--mount",
"type=bind,source=" + job + ",target=/job",
...sourceMounts,
"--env",
"HOME=" + containerPath(home),
"--env",
"XDG_CACHE_HOME=" + containerPath(cache),
"--env",
"DEB_BUILD_OPTIONS=parallel=2 nocheck",
"--workdir",
containerPath(artifacts),
receipt.image.id,
"timeout",
"--kill-after=15",
"7200",
"sbuild",
"--dist=" + profile.series,
"--arch=" + profile.architecture,
...backend,
"--no-clean-source",
"--no-run-lintian",
"--build-dir=" + containerPath(artifacts),
"/source/" + sourceName,
],
job,
"package-build",
7230000,
);
const outputs: { path: string; bytes: number; sha256: string; control: string | null }[] = [];
for await (const name of new Bun.Glob("*").scan({ cwd: artifacts, onlyFiles: true, followSymlinks: false })) {
if (!/\.(deb|ddeb|changes|buildinfo)$/.test(name)) continue;
const proof = await artifactProof(root, join(artifacts, name));
const control = name.endsWith(".deb")
? (
await command(
[
...owner.args,
"run",
"--rm",
"--network=none",
"--cpus=1",
"--memory=1g",
"--pids-limit=128",
"--mount",
"type=bind,source=" + proof.path + ",target=/artifact.deb,readonly",
receipt.image.id,
"dpkg-deb",
"--field",
"/artifact.deb",
"Package",
"Version",
"Architecture",
],
job,
"inspect-package-" + outputs.length,
30000,
)
).stdout
: null;
outputs.push({ ...proof, control });
}
if (!outputs.some(file => file.path.endsWith(".deb"))) throw new Error("sbuild produced no binary package");
const chroots = [];
const chrootCache = join(cache, "sbuild");
if (await Bun.file(join(chrootCache, profile.series + "-" + profile.architecture + ".tar")).exists())
chroots.push(await artifactProof(root, join(chrootCache, profile.series + "-" + profile.architecture + ".tar")));
const nativeDiagnostics: string[] = [];
for await (const name of new Bun.Glob("*.build").scan({
cwd: join(job, "logs"),
onlyFiles: true,
followSymlinks: false,
})) {
const lines = (await Bun.file(join(job, "logs", name)).text()).split(/\r?\n/);
nativeDiagnostics.push(
...lines.filter(line => /mounting \/proc failed|cannot bind-mount \/dev\/console/.test(line)),
);
}
const procMountFailed = nativeDiagnostics.some(line => line.includes("mounting /proc failed"));
return {
schemaVersion: 1,
kind: "ubuntu-package-build",
status: "complete",
root,
jobPath: job,
imageId: receipt.image.id,
backend: flags.chroot ? "schroot" : "unshare",
buildOptions: "parallel=2 nocheck",
input,
sourceFiles,
namespace: {
status: procMountFailed ? "limited" : "no-mount-errors-reported",
procMountFailed,
diagnostics: [...new Set(nativeDiagnostics)],
},
artifacts: outputs,
chroots,
qualification: {
binaryPackagesBuilt: true,
cleanSbuildChroot: chroots.length > 0,
rootfsBuilt: false,
osImageBuilt: false,
},
};
} else {
if (positional.length || !flags.config) throw new Error("Expected owned prepared live-build configuration");
const config = owned(root, await realpath(resolve(flags.config)));
await sameDevice(root, config);
await command(
[...common, "--workdir", "/factory/" + relative(root, config), receipt.image.id, "lb", "build"],
job,
"rootfs-build",
14400000,
);
}
return {
schemaVersion: 1,
kind: action + "-command-complete",
root,
jobPath: job,
imageId: receipt.image.id,
qualification: "Inspect and hash resulting artifacts before product qualification",
};
}
async function main() {
const [action = "help", ...values] = Bun.argv.slice(2);
if (action === "help" || action === "--help") {
console.log(usage);
return;
}
if (process.platform !== "linux")
throw new Error("Run the Bun factory on the selected Linux native/SSH/Docker context");
const { flags, positional } = parse(values);
if (!flags.root || !isAbsolute(flags.root)) throw new Error("Explicit absolute factory --root required");
const requested = resolve(flags.root);
if (requested === "/" || requested === Bun.env.HOME) throw new Error("Select a dedicated factory directory");
await mkdir(requested, { recursive: true });
const root = await realpath(requested);
const profile: FactoryProfile = await Bun.file(join(import.meta.dir, "profile.json")).json();
if (
profile.schemaVersion !== 1 ||
!/^[a-z][a-z0-9-]*$/.test(profile.series) ||
!/^[a-z0-9]+$/.test(profile.architecture) ||
!/^[a-z][a-z0-9_-]*$/.test(profile.packageUser) ||
profile.sourceComponents.some(value => !/^[a-z]+$/.test(value)) ||
profile.toolchainPackages.some(value => !/^[a-z0-9][a-z0-9+.-]+$/.test(value))
)
throw new Error("Invalid factory profile");
const filesystem = await statfs(root, { bigint: true }),
freeBytes = filesystem.bavail * filesystem.bsize;
if (action === "status") {
console.log(
JSON.stringify(
{
root,
freeBytes: freeBytes.toString(),
requiredBytes: profile.minimumFreeBytes,
profile,
lock: (await Bun.file(join(root, ".factory.lock")).exists())
? await Bun.file(join(root, ".factory.lock")).text()
: null,
engine: (await Bun.file(join(root, "engine/launcher.pid")).exists()) ? await engineRecord(root) : null,
hostTools: ["apt-get", "apt-cache", "dpkg-source", "gpgv", "docker", "dockerd", "timeout", "flock"].map(
name => ({ name, path: Bun.which(name) }),
),
},
null,
2,
),
);
return;
}
if (!["sources", "engine", "toolchain", "package", "rootfs"].includes(action))
throw new Error("Unknown factory action " + action);
if (freeBytes < BigInt(profile.minimumFreeBytes)) throw new Error("Factory filesystem has less than 50 GiB free");
if (["toolchain", "package", "rootfs"].includes(action) && Bun.env.BUN_OS_SHARED_LOCK_HELD !== "1") {
const temporary = join(root, "tmp");
await mkdir(temporary, { recursive: true });
await sameDevice(root, temporary);
const child = Bun.spawn({
cmd: [
"flock",
"-n",
resolve(root, profile.sharedHeavyLockRelative),
process.execPath,
import.meta.path,
...Bun.argv.slice(2),
],
cwd: root,
env: {
...Bun.env,
TMPDIR: temporary,
TMP: temporary,
TEMP: temporary,
BUN_OS_SHARED_LOCK_HELD: "1",
BUN_RUNTIME_TRANSPILER_CACHE_PATH: join(root, "cache/transpiler"),
},
stdout: "inherit",
stderr: "inherit",
timeout: action === "rootfs" ? 15000000 : action === "package" ? 7800000 : 2400000,
});
process.exitCode = await child.exited;
if (process.exitCode !== 0)
console.error(
"Shared factory execution failed exit=" +
process.exitCode +
"; lock=" +
resolve(root, profile.sharedHeavyLockRelative),
);
return;
}
const lockFile = join(root, ".factory.lock"),
lock = await open(lockFile, "wx");
const startedAt = new Date().toISOString();
const job = owned(
root,
join(root, action + "-jobs", startedAt.replace(/[:.]/g, "") + "-" + process.pid + "-" + crypto.randomUUID()),
);
const token = JSON.stringify({ pid: process.pid, action, jobPath: job, startedAt });
await lock.writeFile(token + "\n");
try {
for (const path of ["tmp", "logs", "cache", "docker-config", "gnupg"])
await mkdir(join(job, path), { recursive: true });
await sameDevice(root, job);
const selected = positional.length ? positional : profile.sourcePackages;
if (action === "sources" && (!selected.length || selected.some(name => !/^[a-z0-9][a-z0-9+.-]+$/.test(name))))
throw new Error("Expected Debian source package names");
if (action === "sources") await configureSources(job, profile);
const result =
action === "sources"
? await fetchSources(job, root, profile, selected)
: action === "engine"
? await launchEngine(root, job, profile)
: action === "toolchain"
? await provisionTools(root, job, profile)
: await buildContainer(root, job, profile, action, positional, flags);
await Bun.write(join(job, "receipt.json"), JSON.stringify(result, null, 2) + "\n");
await Bun.write(
join(job, "status.json"),
JSON.stringify(
{
schemaVersion: 1,
status: "complete",
step: "complete",
exitCode: 0,
pid: process.pid,
jobPath: job,
receiptPath: join(job, "receipt.json"),
},
null,
2,
) + "\n",
);
console.log(JSON.stringify(result, null, 2));
} catch (error) {
await Bun.write(
join(job, "status.json"),
JSON.stringify(
{ schemaVersion: 1, status: "failed", exitCode: 1, pid: process.pid, jobPath: job, error: String(error) },
null,
2,
) + "\n",
);
throw error;
} finally {
await lock.close();
if ((await Bun.file(lockFile).text()).trim() === token) await unlink(lockFile);
}
}
if (import.meta.main)
main().catch(error => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
+627
View File
@@ -0,0 +1,627 @@
// SPDX-License-Identifier: MIT
import { lstat, mkdir, open, realpath, unlink } from "node:fs/promises";
import { basename, dirname, join, resolve } from "node:path";
import { hostname } from "node:os";
import { isBuiltin } from "node:module";
import { command, identity, packageRoot, type RuntimeReceipt } from "../src/distribution.ts";
import { inspectLinuxABI } from "../src/elf.ts";
import { abi } from "../packages/package-converter/src/abi.ts";
import {
collectAssets,
MAX_OBJECT_BYTES,
inside,
protectedInput,
readInventory,
workspace,
type AssetInventory,
type AssetPlan,
} from "./assets.ts";
export type AppTarget = "windows-x64" | "linux-x64" | "linux-arm64" | "macos-arm64";
export interface RuntimeSnapshot {
path: string;
sha256: string;
revision: string;
version: string;
qualificationReceipt?: string;
}
export interface NativeDependency {
path: string;
sha256: string;
name?: string;
role?: "library" | "executable";
abi?: string;
minimumGlibc?: string;
}
export interface NativeArtifact extends NativeDependency {
sourceRevision: string;
qualificationReceipt: string;
dependencies?: NativeDependency[];
systemLibraries?: string[];
abi?: string;
minimumGlibc?: string;
}
export interface AppEntry {
id: string;
kind: "native-cli" | "bun-cli" | "bun-server";
hostname: string;
workspace: string;
entrypoint: string;
entrypointWorkspace?: string;
assetRoots: string[];
source: { repository: string; revision?: string; path: string; license: string };
native: { required: boolean; binary?: string | null; artifactsByTarget?: Partial<Record<AppTarget, NativeArtifact>> };
externalContext?: string[];
}
export interface AppInventory extends AssetInventory {
targets: AppTarget[];
apps: AppEntry[];
runtimeSnapshots?: Partial<Record<AppTarget, RuntimeSnapshot>>;
}
export interface EmbeddedNativeFile {
path: string;
name: string;
sha256: string;
bytes: number;
kind: "backend" | "dependency";
role: "library" | "executable";
abi?: string;
minimumGlibc?: string;
}
export interface NativeLaunchContract {
appId: string;
target: AppTarget;
files: Omit<EmbeddedNativeFile, "path">[];
}
export interface AppPlan {
schemaVersion: 1;
kind: "app-build-plan";
configPath: string;
configSHA256: string;
app: AppEntry;
target: AppTarget;
cwd: string;
entrypoint: string;
outputRoot: string;
status: "prepared" | "blocked";
reasons: string[];
runtime?: {
path: string;
bytes: number;
sha256: string;
version: string;
revision: string;
metadataExecution: string;
reportedSourceUrl?: string;
};
nativeFiles: EmbeddedNativeFile[];
nativeEvidence?: unknown;
assets: AssetPlan;
qualification: { binaryBuilt: false; deployed: false };
}
export function nativeTarget(): AppTarget | undefined {
if (process.platform === "win32" && process.arch === "x64") return "windows-x64";
if (process.platform === "linux" && ["x64", "arm64"].includes(process.arch))
return ("linux-" + process.arch) as AppTarget;
if (process.platform === "darwin" && process.arch === "arm64") return "macos-arm64";
}
export async function executableABI(path: string, target: AppTarget, library = false) {
const size = (await lstat(path)).size;
if (size > 512 * 1024 * 1024) throw new Error("Executable exceeds the pinned inspection limit");
const actual = abi(await Bun.file(path).bytes());
const expected = target.endsWith("arm64") ? "arm64" : "x64";
if (target === "macos-arm64") throw new Error("The shared ABI owner does not yet qualify Mach-O");
if (actual.architecture !== expected || actual.bits !== 64)
throw new Error("Executable architecture differs from its target");
if (target === "windows-x64") {
if (actual.format !== "pe" || actual.runtime !== "windows" || (library ? !actual.library : !actual.executable))
throw new Error("Windows target needs a qualified native PE artifact; alternate runtimes remain distinct");
return actual;
}
if (actual.format !== "elf") throw new Error("Linux target needs ELF artifact evidence");
return library ? actual : { ...actual, ...(await inspectLinuxABI(path, expected)) };
}
async function snapshot(configPath: string, config: AppInventory, target: AppTarget) {
const selected = config.runtimeSnapshots?.[target];
const path = await realpath(selected ? resolve(dirname(configPath), selected.path) : process.execPath);
if (protectedInput(path)) throw new Error("Runtime snapshot points to a protected store");
const digest = await identity(path);
await executableABI(path, target);
if (selected && (!/^[a-f0-9]{64}$/.test(selected.sha256) || selected.sha256 !== digest.sha256))
throw new Error("Runtime snapshot checksum mismatch");
if (!selected && target !== nativeTarget())
throw new Error("Cross-target builds need an explicitly qualified runtime snapshot");
if (target === nativeTarget()) {
const metadata = JSON.parse(
await command(
[
path,
"--eval",
"console.log(JSON.stringify({version:Bun.version,revision:Bun.revision,sourceUrl:process.release.sourceUrl,platform:process.platform,arch:process.arch}))",
],
dirname(path),
15_000,
),
);
if (
!/^[a-f0-9]{40}$/.test(metadata.revision) ||
!String(metadata.sourceUrl).startsWith("https://github.com/aphrody-labs/bun/releases/download/") ||
metadata.revision !== Bun.revision ||
(selected && (selected.revision !== metadata.revision || selected.version !== metadata.version))
)
throw new Error("The selected runtime and compiler provenance do not agree");
const after = await identity(path);
if (after.sha256 !== digest.sha256) throw new Error("Runtime changed during metadata qualification");
return {
path,
...digest,
version: metadata.version as string,
revision: metadata.revision as string,
metadataExecution: "native-host",
reportedSourceUrl: metadata.sourceUrl as string,
};
}
if (!selected?.qualificationReceipt)
throw new Error("Cross-target runtime needs native target qualification evidence");
const receiptPath = await realpath(resolve(dirname(configPath), selected.qualificationReceipt));
if (protectedInput(receiptPath)) throw new Error("Runtime receipt points to a protected store");
const receipt = (await Bun.file(receiptPath).json()) as RuntimeReceipt;
if (
receipt.schemaVersion !== 1 ||
receipt.repository !== "aphrody-labs/bun" ||
receipt.target !== target ||
receipt.sha256 !== digest.sha256 ||
receipt.bytes !== digest.bytes ||
receipt.revision !== selected.revision ||
receipt.version !== selected.version ||
receipt.revision !== Bun.revision ||
!receipt.qualification?.passed ||
!receipt.qualification.gates.includes("native-runtime")
)
throw new Error("Cross-target snapshot receipt does not match its exact bytes, target and compiler");
return {
path,
...digest,
version: receipt.version,
revision: receipt.revision,
metadataExecution: "qualified-target-receipt",
};
}
async function nativeArtifacts(configPath: string, app: AppEntry, target: AppTarget) {
const descriptor = app.native.artifactsByTarget?.[target];
if (!app.native.binary) throw new Error("No native executable capability owner is configured for this app");
if (!descriptor) throw new Error("Native backend has no qualified artifact descriptor");
if (!app.source.revision || descriptor.sourceRevision !== app.source.revision)
throw new Error("Native source pin mismatch");
const files: EmbeddedNativeFile[] = [],
basenames = new Set<string>();
for (const [index, input] of [descriptor, ...(descriptor.dependencies ?? [])].entries()) {
const path = await realpath(resolve(dirname(configPath), input.path));
if (protectedInput(path)) throw new Error("Native payload points to a protected store");
const digest = await identity(path);
if (digest.bytes < 1 || digest.bytes > MAX_OBJECT_BYTES)
throw new Error("Native payload exceeds the standalone materialization limit");
if (digest.sha256 !== input.sha256) throw new Error("Native payload checksum mismatch");
const name = input.name ?? basename(path);
if (!/^[a-zA-Z0-9_.+-]+$/.test(name) || basenames.has(name.toLowerCase()))
throw new Error("Native loader filenames must be unique");
basenames.add(name.toLowerCase());
await executableABI(path, target, index > 0 && input.role !== "executable");
files.push({
path,
name,
...digest,
kind: index ? "dependency" : "backend",
role: index ? (input.role ?? "library") : "executable",
abi: input.abi,
minimumGlibc: input.minimumGlibc,
});
}
const receiptPath = await realpath(resolve(dirname(configPath), descriptor.qualificationReceipt));
if (protectedInput(receiptPath)) throw new Error("Native receipt points to a protected store");
const receipt = await Bun.file(receiptPath).json();
if (
receipt.schemaVersion !== 1 ||
receipt.target !== target ||
receipt.sha256 !== files[0]!.sha256 ||
receipt.source?.repository !== app.source.repository ||
receipt.source?.revision !== descriptor.sourceRevision ||
!receipt.qualification?.passed ||
!receipt.qualification.gates?.includes("native-backend") ||
!receipt.qualification.gates?.includes("native-dependency-closure")
)
throw new Error("Native backend needs a matching source, artifact and dependency qualification receipt");
const dependencies = files
.slice(1)
.map(({ name, sha256, bytes, role, abi, minimumGlibc }) => ({ name, sha256, bytes, role, abi, minimumGlibc }))
.sort((a, b) => a.name.localeCompare(b.name));
const declared = (receipt.dependencies ?? [])
.map(
(file: {
name: string;
sha256: string;
bytes: number;
role?: "library" | "executable";
abi?: string;
minimumGlibc?: string;
}) => ({
name: file.name,
sha256: file.sha256,
bytes: file.bytes,
role: file.role ?? "library",
abi: file.abi,
minimumGlibc: file.minimumGlibc,
}),
)
.sort((a: { name: string }, b: { name: string }) => a.name.localeCompare(b.name));
if (
JSON.stringify(dependencies) !== JSON.stringify(declared) ||
JSON.stringify([...(descriptor.systemLibraries ?? [])].sort()) !==
JSON.stringify([...(receipt.systemLibraries ?? [])].sort()) ||
(descriptor.abi && receipt.abi !== descriptor.abi) ||
(descriptor.minimumGlibc && receipt.minimumGlibc !== descriptor.minimumGlibc)
)
throw new Error("Native dependency closure or ABI version differs from its qualified producer receipt");
return {
files,
evidence: {
receipt: descriptor.qualificationReceipt,
abi: descriptor.abi,
minimumGlibc: descriptor.minimumGlibc,
systemLibraries: descriptor.systemLibraries ?? [],
source: receipt.source,
},
};
}
export async function planApp(configPath: string, appId: string, target: AppTarget): Promise<AppPlan> {
configPath = resolve(configPath);
const config = (await readInventory(configPath)) as AppInventory;
const app = config.apps.find(app => app.id === appId);
if (!app || !config.targets.includes(target)) throw new Error("Unknown app or target");
const root = await workspace(configPath, config, app.entrypointWorkspace ?? app.workspace);
let entrypoint = inside(root, resolve(root, app.entrypoint));
const reasons: string[] = [];
if (!(await Bun.file(entrypoint).exists())) reasons.push("Configured entrypoint is absent");
else entrypoint = inside(root, await realpath(entrypoint));
if (protectedInput(entrypoint)) reasons.push("Entrypoint points to a protected store");
if (app.externalContext?.length) reasons.push("Unresolved external context: " + app.externalContext.join(", "));
const assets = await collectAssets(configPath, [app.id]);
let runtime: AppPlan["runtime"],
nativeFiles: EmbeddedNativeFile[] = [],
nativeEvidence: unknown;
try {
runtime = await snapshot(configPath, config, target);
} catch (error) {
reasons.push(error instanceof Error ? error.message : "Runtime snapshot unavailable");
}
if (app.native.required) {
try {
const selected = await nativeArtifacts(configPath, app, target);
nativeFiles = selected.files;
nativeEvidence = selected.evidence;
} catch (error) {
reasons.push(error instanceof Error ? error.message : "Native backend unqualified");
}
}
return {
schemaVersion: 1,
kind: "app-build-plan",
configPath,
configSHA256: (await identity(configPath)).sha256,
app,
target,
cwd: root,
entrypoint,
outputRoot: resolve(dirname(configPath), config.build?.outputRoot ?? "../../build/bun-os-apps"),
status: reasons.length ? "blocked" : "prepared",
reasons,
runtime,
nativeFiles,
nativeEvidence,
assets,
qualification: { binaryBuilt: false, deployed: false },
};
}
async function compileWorker(planPath: string, output: string) {
const plan = (await Bun.file(planPath).json()) as AppPlan;
if (plan.status !== "prepared" || !plan.runtime) throw new Error("Prepared app plan and runtime snapshot required");
const current = await planApp(plan.configPath, plan.app.id, plan.target);
if (JSON.stringify(current) !== JSON.stringify(plan)) throw new Error("App inputs changed before compilation");
const proofs = new Map<string, { bytes: number; sha256: string }>();
const contract: NativeLaunchContract = {
appId: plan.app.id,
target: plan.target,
files: plan.nativeFiles.map(({ path: _path, ...file }) => file),
};
const outfile = join(output, plan.app.id + (plan.target === "windows-x64" ? ".exe" : ""));
const target = (plan.target === "macos-arm64" ? "bun-darwin-arm64" : "bun-" + plan.target) as Bun.Build.CompileTarget;
const result = await Bun.build({
entrypoints: [plan.entrypoint],
target: "bun",
packages: "bundle",
allowUnresolved: [],
splitting: false,
env: "disable",
minify: true,
metafile: true,
define: { __BUN_OS_NATIVE_CONTRACT__: JSON.stringify(contract) },
compile: {
target,
executablePath: plan.runtime.path,
outfile,
assets: [
...plan.nativeFiles.map(file => file.path),
...plan.assets.objects.flatMap(object => object.sources.slice(0, 1)),
],
autoloadDotenv: false,
autoloadBunfig: false,
autoloadPackageJson: false,
autoloadTsconfig: false,
},
plugins: [
{
name: "app-input-proof",
setup(build) {
build.onLoad({ filter: /.*/, namespace: "file" }, async args => {
if (protectedInput(args.path)) throw new Error("Bundler input points to a protected store");
proofs.set(args.path, await identity(args.path));
return undefined;
});
},
},
],
});
if (!result.success || !result.metafile) throw new Error("Native compile failed or emitted no source graph");
const unresolved = Object.values(result.metafile.inputs)
.flatMap(input => input.imports)
.filter(item => item.external && !item.path.startsWith("bun:") && item.path !== "bun" && !isBuiltin(item.path));
if (unresolved.length) throw new Error("Compiled app still has external JavaScript imports");
const provenPaths = new Set([...proofs.keys()].map(path => resolve(path)));
for (const path of Object.keys(result.metafile.inputs)) {
if (path.startsWith("<") || path.startsWith("bun:") || path === "bun" || isBuiltin(path)) continue;
if (!provenPaths.has(resolve(plan.cwd, path)))
throw new Error("Bundler source graph lacks an input checksum proof");
}
const sources = [];
for (const [path, before] of proofs) {
const after = await identity(path);
if (before.sha256 !== after.sha256 || before.bytes !== after.bytes)
throw new Error("A source input changed during compilation");
sources.push({ path, ...after });
}
for (const input of [
...plan.nativeFiles,
...plan.assets.objects.map(object => ({ path: object.sources[0]!, ...object })),
]) {
const after = await identity(input.path);
if (after.sha256 !== input.sha256 || after.bytes !== input.bytes)
throw new Error("An embedded payload changed during compilation");
}
if ((await identity(plan.runtime.path)).sha256 !== plan.runtime.sha256)
throw new Error("Runtime changed during compilation");
const artifactABI = await executableABI(outfile, plan.target);
const artifact = { path: outfile, ...(await identity(outfile)), abi: artifactABI };
const receipt = {
schemaVersion: 1,
kind: "app-build",
status: "complete",
appId: plan.app.id,
target: plan.target,
source: plan.app.source,
sources,
runtime: plan.runtime,
artifact,
nativeFiles: plan.nativeFiles,
nativeEvidence: plan.nativeEvidence,
assets: plan.assets.apps[0],
nativeContract: contract,
qualification: {
binaryBuilt: true,
artifactHeaders: true,
runtimeSnapshot: true,
bundledJavaScript: true,
nativeBackendRequired: plan.app.native.required,
nativeBackendEmbedded: plan.app.native.required && !!plan.nativeFiles.length,
nativeExecution: false,
appBehavior: false,
assetsPublicDelivery: false,
deployed: false,
},
};
await Bun.write(join(output, "metafile.json"), JSON.stringify(result.metafile, null, 2) + "\n");
await Bun.write(join(output, "receipt.json"), JSON.stringify(receipt, null, 2) + "\n");
return receipt;
}
export async function buildApp(configPath: string, appId: string, target: AppTarget, requestedOutput?: string) {
const config = (await readInventory(configPath)) as AppInventory,
plan = await planApp(configPath, appId, target);
if (plan.status !== "prepared") throw new Error(plan.reasons.join("; "));
const output = resolve(
requestedOutput ?? join(plan.outputRoot, target, appId, new Date().toISOString().replace(/[:.]/g, "")),
);
try {
await lstat(output);
throw new Error("Choose a fresh app output directory");
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
const timeout = (config.build?.timeoutSeconds ?? 600) * 1000;
if (!Number.isSafeInteger(timeout) || timeout < 1000 || timeout > 3_600_000)
throw new Error("Invalid finite compile deadline");
if (!config.build?.sharedHeavyLock || !config.build.lockKind)
throw new Error("Configure the existing host heavy lock owner");
await mkdir(output, { recursive: true });
for (const directory of ["tmp", "logs"]) await mkdir(join(output, directory));
const factoryLock = await open(join(packageRoot, ".factory.lock"), "wx");
const token = JSON.stringify({ pid: process.pid, operation: "app-build", appId, target, output });
await factoryLock.writeFile(token + "\n");
let heavy: Awaited<ReturnType<typeof open>> | undefined;
let heavyPath: string | undefined;
try {
const planPath = join(output, "plan.json");
await Bun.write(planPath, JSON.stringify(plan, null, 2) + "\n");
const args = [process.execPath, import.meta.path, "_compile", "--plan", planPath, "--out", output];
const lock = resolve(dirname(configPath), config.build.sharedHeavyLock);
if (config.build.lockKind === "exclusive-file") {
heavyPath = lock;
heavy = await open(lock, "wx");
await heavy.writeFile(process.pid + "\napp-build " + appId + "\n" + hostname() + "\n");
} else {
if (process.platform !== "linux" || !Bun.which("flock"))
throw new Error("Selected host has no native flock owner");
args.unshift("flock", "-n", lock);
}
const [stdout, stderr] = await Promise.all([
open(join(output, "logs/compile.stdout"), "w"),
open(join(output, "logs/compile.stderr"), "w"),
]);
try {
const child = Bun.spawn({
cmd: args,
cwd: plan.cwd,
env: {
...Bun.env,
TMPDIR: join(output, "tmp"),
TMP: join(output, "tmp"),
TEMP: join(output, "tmp"),
BUN_RUNTIME_TRANSPILER_CACHE_PATH: join(output, "tmp/transpiler"),
},
stdin: "ignore",
stdout: stdout.fd,
stderr: stderr.fd,
windowsHide: true,
timeout,
});
await Bun.write(
join(output, "status.json"),
JSON.stringify(
{
status: "running",
pid: process.pid,
childPid: child.pid,
output,
appId,
target,
deadlineSeconds: timeout / 1000,
},
null,
2,
) + "\n",
);
const code = await child.exited;
await Bun.write(
join(output, "status.json"),
JSON.stringify(
{
status: code === 0 ? "complete" : "failed",
exitCode: code,
pid: process.pid,
childPid: child.pid,
output,
appId,
target,
},
null,
2,
) + "\n",
);
if (code !== 0) throw new Error("Bounded app compilation failed; inspect its owned logs");
} finally {
await Promise.all([stdout.close(), stderr.close()]);
}
return Bun.file(join(output, "receipt.json")).json();
} finally {
if (heavy) {
await heavy.close();
if ((await Bun.file(heavyPath!).text()).startsWith(process.pid + "\n")) await unlink(heavyPath!);
}
await factoryLock.close();
if ((await Bun.file(join(packageRoot, ".factory.lock")).text()).trim() === token)
await unlink(join(packageRoot, ".factory.lock"));
}
}
export async function stageAppRelease(configPath: string, receiptPath: string) {
const config = (await readInventory(configPath)) as AppInventory,
receipt = await Bun.file(receiptPath).json();
if (
receipt.schemaVersion !== 1 ||
receipt.kind !== "app-build" ||
receipt.status !== "complete" ||
!receipt.qualification?.binaryBuilt ||
!config.apps.some(app => app.id === receipt.appId)
)
throw new Error("An actual app build receipt is required");
const digest = await identity(receipt.artifact.path);
if (digest.sha256 !== receipt.artifact.sha256 || digest.bytes !== receipt.artifact.bytes)
throw new Error("Release artifact changed");
await executableABI(receipt.artifact.path, receipt.target);
return {
schemaVersion: 1,
kind: "app-release-stage",
appId: receipt.appId,
target: receipt.target,
source: receipt.source,
artifact: {
...digest,
objectKey: config.storage.artifacts.prefix + digest.sha256,
url: config.storage.artifacts.origin.replace(/\/$/, "") + "/" + config.storage.artifacts.prefix + digest.sha256,
},
runtime: { version: receipt.runtime.version, revision: receipt.runtime.revision, sha256: receipt.runtime.sha256 },
native: receipt.nativeContract,
assets: receipt.assets,
qualification: { ...receipt.qualification, uploaded: false },
catalogPublication: "owned by the edge/catalog capability",
};
}
async function main() {
const [action = "--help", ...args] = Bun.argv.slice(2);
if (action === "--help") {
console.log(
"bun infra/app-build.ts plan|build|release APP --config FILE --target TARGET [--out NEW_DIRECTORY] [--receipt FILE]",
);
return;
}
const values: Record<string, string> = {},
positional: string[] = [];
for (let index = 0; index < args.length; index++) {
const arg = args[index]!;
if (!arg.startsWith("--")) {
positional.push(arg);
continue;
}
const value = args[++index];
if (!["--config", "--target", "--out", "--plan", "--receipt"].includes(arg) || !value || values[arg])
throw new Error("Invalid app build option");
values[arg] = value;
}
if (action === "_compile") {
await compileWorker(resolve(values["--plan"]!), resolve(values["--out"]!));
return;
}
if (!values["--config"] || positional.length !== 1) throw new Error("Explicit config and one app required");
const config = resolve(values["--config"]),
id = positional[0]!;
if (action === "release") {
if (!values["--receipt"] || !values["--out"])
throw new Error("Release staging needs an artifact receipt and output file");
const staged = await stageAppRelease(config, resolve(values["--receipt"]));
if (staged.appId !== id) throw new Error("Release app identity mismatch");
await Bun.write(resolve(values["--out"]), JSON.stringify(staged, null, 2) + "\n");
return;
}
if (!values["--target"]) throw new Error("Explicit app target required");
const target = values["--target"] as AppTarget;
const result =
action === "plan"
? await planApp(config, id, target)
: action === "build"
? await buildApp(config, id, target, values["--out"])
: null;
if (!result) throw new Error("Unknown app build action");
console.log(JSON.stringify(result, null, 2));
}
if (import.meta.main)
main().catch(error => {
console.error(error instanceof Error ? error.message : "App build failed");
process.exitCode = 1;
});
+454
View File
@@ -0,0 +1,454 @@
{
"schemaVersion": 1,
"baseDomain": "aphrody.app",
"cloudflare": {
"accountId": "ca22c1a51a145f919b54afa18d936516",
"zoneId": "f4dab237b9811435da6fee0b5b3438d4",
"worker": "aphrody-app"
},
"workspaces": {
"os": "..",
"libraries": "../../bun-libraries",
"material": "../../material"
},
"storage": {
"assets": {
"bucket": "aphrody-assets",
"origin": "https://assets.aphrody.app",
"prefix": "objects/sha256/"
},
"artifacts": {
"bucket": "aphrody-dist",
"origin": "https://dist.aphrody.app",
"prefix": "objects/sha256/"
}
},
"targets": ["windows-x64", "linux-x64", "linux-arm64", "macos-arm64"],
"apps": [
{
"id": "filmcraft",
"hostname": "filmcraft.aphrody.app",
"kind": "native-cli",
"workspace": "libraries",
"entrypoint": "infra/native-app.ts",
"entrypointWorkspace": "os",
"source": {
"repository": "aphrody-labs/filmcraft",
"revision": "87b845138e2cecf43e97e2c2e011b820a6e70ecd",
"path": "libraries/filmcraft",
"license": "MIT OR Apache-2.0"
},
"native": {
"required": true,
"binary": "filmcraft-cli",
"cargoPackage": "filmcraft-cli",
"manifestPath": "Cargo.toml"
},
"assetRoots": [],
"qualification": {
"nativeBackend": false,
"standaloneBinary": false,
"deployed": false
}
},
{
"id": "artcraft",
"hostname": "artcraft.aphrody.app",
"kind": "native-cli",
"workspace": "libraries",
"entrypoint": "infra/native-app.ts",
"entrypointWorkspace": "os",
"source": {
"repository": "aphrody-labs/artcraft",
"revision": "f8dae6cec51cc84c8b565132db2c7471fad96148",
"path": "libraries/artcraft",
"license": "MIT OR Apache-2.0"
},
"native": {
"required": true,
"binary": null,
"cargoPackage": "artcraft_client",
"manifestPath": "Cargo.toml"
},
"assetRoots": [],
"qualification": {
"nativeBackend": false,
"standaloneBinary": false,
"deployed": false
}
},
{
"id": "photocraft",
"hostname": "photocraft.aphrody.app",
"kind": "native-cli",
"workspace": "libraries",
"entrypoint": "infra/native-app.ts",
"entrypointWorkspace": "os",
"source": {
"repository": "aphrody-labs/photocraft",
"revision": "6f85054288a564f9fbbccb99268ffdb34e581584",
"path": "libraries/photocraft",
"license": "MIT OR Apache-2.0"
},
"native": {
"required": true,
"binary": "photocraft-cli",
"cargoPackage": "photocraft-cli",
"manifestPath": "Cargo.toml"
},
"assetRoots": [],
"qualification": {
"nativeBackend": false,
"standaloneBinary": false,
"deployed": false
}
},
{
"id": "vectorcraft",
"hostname": "vectorcraft.aphrody.app",
"kind": "native-cli",
"workspace": "libraries",
"entrypoint": "infra/native-app.ts",
"entrypointWorkspace": "os",
"source": {
"repository": "aphrody-labs/vectorcraft",
"revision": "0d0ea352df99044589a7ce0efc7004f7a5b6b7c0",
"path": "libraries/vectorcraft",
"license": "MIT OR Apache-2.0"
},
"native": {
"required": true,
"binary": "vectorcraft-cli",
"cargoPackage": "vectorcraft-cli",
"manifestPath": "Cargo.toml"
},
"assetRoots": [],
"qualification": {
"nativeBackend": false,
"standaloneBinary": false,
"deployed": false
}
},
{
"id": "effectcraft",
"hostname": "effectcraft.aphrody.app",
"kind": "native-cli",
"workspace": "libraries",
"entrypoint": "infra/native-app.ts",
"entrypointWorkspace": "os",
"source": {
"repository": "aphrody-labs/effectcraft",
"revision": "e5beea940ed245dc8e6c9293d6f7717b6c3f2724",
"path": "libraries/effectcraft",
"license": "MIT OR Apache-2.0"
},
"native": {
"required": true,
"binary": "effectcraft-cli",
"cargoPackage": "effectcraft-cli",
"manifestPath": "Cargo.toml"
},
"assetRoots": [],
"qualification": {
"nativeBackend": false,
"standaloneBinary": false,
"deployed": false
}
},
{
"id": "lightcraft",
"hostname": "lightcraft.aphrody.app",
"kind": "native-cli",
"workspace": "libraries",
"entrypoint": "infra/native-app.ts",
"entrypointWorkspace": "os",
"source": {
"repository": "aphrody-labs/lightcraft",
"revision": "b3d392b4994ee99dc389890f006600769d0c4ff5",
"path": "libraries/lightcraft",
"license": "MIT OR Apache-2.0"
},
"native": {
"required": true,
"binary": "lightcraft-cli",
"cargoPackage": "lightcraft-cli",
"manifestPath": "Cargo.toml"
},
"assetRoots": [],
"qualification": {
"nativeBackend": false,
"standaloneBinary": false,
"deployed": false
}
},
{
"id": "pdfcraft",
"hostname": "pdfcraft.aphrody.app",
"kind": "native-cli",
"workspace": "libraries",
"entrypoint": "infra/native-app.ts",
"entrypointWorkspace": "os",
"source": {
"repository": "aphrody-labs/pdfcraft",
"revision": "7437bde378f43856eada40af25b983812e615e8a",
"path": "libraries/pdfcraft",
"license": "MIT OR Apache-2.0"
},
"native": {
"required": true,
"binary": "pdfcraft-cli",
"cargoPackage": "pdfcraft-cli",
"manifestPath": "Cargo.toml"
},
"assetRoots": [],
"qualification": {
"nativeBackend": false,
"standaloneBinary": false,
"deployed": false
}
},
{
"id": "wordcraft",
"hostname": "wordcraft.aphrody.app",
"kind": "native-cli",
"workspace": "libraries",
"entrypoint": "infra/native-app.ts",
"entrypointWorkspace": "os",
"source": {
"repository": "aphrody-labs/wordcraft",
"revision": "159b84f54c74e9e82fb3109b758ae7dd6f4e3ab0",
"path": "libraries/wordcraft",
"license": "MIT OR Apache-2.0"
},
"native": {
"required": true,
"binary": "wordcraft-cli",
"cargoPackage": "wordcraft-cli",
"manifestPath": "Cargo.toml"
},
"assetRoots": [],
"qualification": {
"nativeBackend": false,
"standaloneBinary": false,
"deployed": false
}
},
{
"id": "soundcraft",
"hostname": "soundcraft.aphrody.app",
"kind": "native-cli",
"workspace": "libraries",
"entrypoint": "infra/native-app.ts",
"entrypointWorkspace": "os",
"source": {
"repository": "aphrody-labs/soundcraft",
"revision": "6cf896e76c0ec82a11bf2494f209cfe7c8507f74",
"path": "libraries/soundcraft",
"license": "MIT OR Apache-2.0"
},
"native": {
"required": true,
"binary": "soundcraft-cli",
"cargoPackage": "soundcraft-cli",
"manifestPath": "Cargo.toml"
},
"assetRoots": [],
"qualification": {
"nativeBackend": false,
"standaloneBinary": false,
"deployed": false
}
},
{
"id": "designcraft",
"hostname": "designcraft.aphrody.app",
"kind": "native-cli",
"workspace": "libraries",
"entrypoint": "infra/native-app.ts",
"entrypointWorkspace": "os",
"source": {
"repository": "aphrody-labs/designcraft",
"revision": "e862e0a613c0793c6fea7cfd37ad4145ed6cf686",
"path": "libraries/designcraft",
"license": "MIT OR Apache-2.0"
},
"native": {
"required": true,
"binary": "designcraft-cli",
"cargoPackage": "designcraft-cli",
"manifestPath": "Cargo.toml"
},
"assetRoots": [],
"qualification": {
"nativeBackend": false,
"standaloneBinary": false,
"deployed": false
}
},
{
"id": "cadcraft",
"hostname": "cadcraft.aphrody.app",
"kind": "native-cli",
"workspace": "libraries",
"entrypoint": "infra/native-app.ts",
"entrypointWorkspace": "os",
"source": {
"repository": "aphrody-labs/cadcraft",
"revision": "00c7613f1af3c73f7157fa91d9fe2635c6f23c06",
"path": "libraries/cadcraft",
"license": "MIT OR Apache-2.0"
},
"native": {
"required": true,
"binary": "cadcraft-cli",
"cargoPackage": "cadcraft-cli",
"manifestPath": "Cargo.toml"
},
"assetRoots": [],
"qualification": {
"nativeBackend": false,
"standaloneBinary": false,
"deployed": false
}
},
{
"id": "deckcraft",
"hostname": "deckcraft.aphrody.app",
"kind": "native-cli",
"workspace": "libraries",
"entrypoint": "infra/native-app.ts",
"entrypointWorkspace": "os",
"source": {
"repository": "aphrody-labs/deckcraft",
"revision": "1660982e58ee48fd9f9bef6dc2620d78ada04613",
"path": "libraries/deckcraft",
"license": "MIT OR Apache-2.0"
},
"native": {
"required": true,
"binary": "deckcraft-cli",
"cargoPackage": "deckcraft-cli",
"manifestPath": "Cargo.toml"
},
"assetRoots": [],
"qualification": {
"nativeBackend": false,
"standaloneBinary": false,
"deployed": false
}
},
{
"id": "gridcraft",
"hostname": "gridcraft.aphrody.app",
"kind": "native-cli",
"workspace": "libraries",
"entrypoint": "infra/native-app.ts",
"entrypointWorkspace": "os",
"source": {
"repository": "aphrody-labs/gridcraft",
"revision": "84acf6293b1dc1fc09837524e035d674eb4d6741",
"path": "libraries/gridcraft",
"license": "MIT OR Apache-2.0"
},
"native": {
"required": true,
"binary": "gridcraft-cli",
"cargoPackage": "gridcraft-cli",
"manifestPath": "Cargo.toml"
},
"assetRoots": [],
"qualification": {
"nativeBackend": false,
"standaloneBinary": false,
"deployed": false
}
},
{
"id": "figcraft",
"hostname": "figcraft.aphrody.app",
"kind": "native-cli",
"workspace": "libraries",
"entrypoint": "infra/native-app.ts",
"entrypointWorkspace": "os",
"source": {
"repository": "aphrody-labs/figcraft",
"revision": "e56856f4016c76fbc862ac2fb4fd3fb43a68f396",
"path": "libraries/figcraft",
"license": "MIT OR Apache-2.0"
},
"native": {
"required": true,
"binary": "figcraft-cli",
"cargoPackage": "figcraft-cli",
"manifestPath": "Cargo.toml"
},
"assetRoots": [],
"qualification": {
"nativeBackend": false,
"standaloneBinary": false,
"deployed": false
}
},
{
"id": "apt",
"hostname": "apt.aphrody.app",
"kind": "bun-cli",
"workspace": "os",
"entrypoint": "src/apt.ts",
"entrypointWorkspace": "os",
"source": {
"repository": "aphrody-labs/bun-distribution",
"path": ".",
"license": "MIT"
},
"native": {
"required": false
},
"assetRoots": [],
"qualification": {
"nativeBackend": false,
"standaloneBinary": true,
"deployed": false
}
},
{
"id": "desktop",
"hostname": "desktop.aphrody.app",
"kind": "bun-server",
"workspace": "libraries",
"entrypoint": "apps/desktop/server.ts",
"entrypointWorkspace": "libraries",
"source": {
"repository": "aphrody-labs/bun-libraries",
"path": "apps/desktop",
"license": "MIT"
},
"native": {
"required": true,
"binary": null
},
"assetRoots": ["apps/desktop/web"],
"qualification": {
"nativeBackend": false,
"standaloneBinary": false,
"deployed": false
},
"externalContext": ["workspace-catalog", "qualified-native-engines"]
}
],
"build": {
"outputRoot": "../../build/bun-os-apps",
"timeoutSeconds": 600,
"sharedHeavyLock": "../../../bun/build/cargo-serial/.build.lock",
"lockKind": "exclusive-file"
},
"runtimeSnapshots": {
"windows-x64": {
"path": "../../../bun/build/release/bun.exe",
"sha256": "a6f79203a9601914e86479f103c7f695330d54442169566f64e29328440d9c89",
"revision": "2ae8fc65fba1d05c423a2433b1195835109942b6",
"version": "1.4.4",
"qualificationReceipt": "../out/runtime-current/windows-x64.json"
}
}
}
+325
View File
@@ -0,0 +1,325 @@
// SPDX-License-Identifier: MIT
import { lstat, mkdir, readdir, realpath } from "node:fs/promises";
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
import { pathToFileURL } from "node:url";
import { contentIdentity, identity, runtimeCheckout } from "../src/distribution.ts";
export interface AssetStorage {
bucket: string;
origin: string;
prefix: string;
}
export interface AssetApp {
id: string;
workspace: string;
assetRoots: string[];
}
export interface AssetInventory {
schemaVersion: 1;
workspaces: Record<string, string>;
storage: { assets: AssetStorage; artifacts: AssetStorage };
apps: AssetApp[];
checkouts?: Record<string, string>;
build?: {
outputRoot?: string;
timeoutSeconds?: number;
sharedHeavyLock?: string;
lockKind?: "flock" | "exclusive-file";
};
}
export interface AssetReference {
path: string;
sha256: string;
bytes: number;
objectKey: string;
type: string;
}
export interface AssetObject {
sha256: string;
bytes: number;
objectKey: string;
sources: string[];
}
export interface AssetPlan {
schemaVersion: 1;
kind: "app-asset-plan";
configSHA256: string;
storage: AssetStorage;
objects: AssetObject[];
apps: { id: string; assets: AssetReference[] }[];
qualification: { uploaded: false; publicDelivery: false };
}
const MAX_FILES = 20_000;
export const MAX_OBJECT_BYTES = 256 * 1024 * 1024;
export function protectedInput(path: string): boolean {
const normalized = path.replaceAll("\\", "/").toLowerCase();
return (
/(?:^|\/)(?:\.codex|\.claude|\.git|cookies|provider-sessions|datasets|weights|game-data)(?:\/|$)/.test(
normalized,
) ||
/(?:^|\/)\.aphrody\/(?:config|cookies|sessions|memory|providers)(?:\/|$)/.test(normalized) ||
/(?:^|\/)(?:\.env(?:\.[^/]*)?|credentials(?:\.[^/]*)?|cookies?(?:\.[^/]*)?)(?:\/|$)/.test(normalized) ||
/\.(?:pem|key|p12|pfx|gguf|safetensors|ckpt|jsonl)$/i.test(normalized)
);
}
export function inside(root: string, path: string, allowRoot = false): string {
const resolved = resolve(path),
suffix = relative(root, resolved);
if (
(!suffix && !allowRoot) ||
suffix === ".." ||
suffix.startsWith(".." + (process.platform === "win32" ? "\\" : "/")) ||
isAbsolute(suffix)
)
throw new Error("Input is outside its explicitly configured root");
return resolved;
}
export async function readInventory(path: string): Promise<AssetInventory> {
const config = (await Bun.file(path).json()) as AssetInventory;
if (config.schemaVersion !== 1 || !config.workspaces || !Array.isArray(config.apps))
throw new Error("Invalid app inventory");
const ids = new Set<string>();
for (const app of config.apps) {
if (
!/^[a-z0-9][a-z0-9-]*$/.test(app.id) ||
ids.has(app.id) ||
!config.workspaces[app.workspace] ||
!Array.isArray(app.assetRoots)
)
throw new Error("Invalid or duplicate app inventory entry");
ids.add(app.id);
for (const path of app.assetRoots)
if (!path || isAbsolute(path) || path.split(/[\\/]/).includes("..") || protectedInput(path))
throw new Error("Asset roots must be explicit public directories inside their workspace");
}
for (const storage of [config.storage.assets, config.storage.artifacts]) {
const origin = new URL(storage.origin);
if (
!/^[a-z0-9][a-z0-9-]{1,61}[a-z0-9]$/.test(storage.bucket) ||
storage.prefix !== "objects/sha256/" ||
origin.protocol !== "https:" ||
origin.username ||
origin.password ||
origin.search ||
origin.hash
)
throw new Error("Invalid shared object storage configuration");
}
return config;
}
export async function workspace(configPath: string, config: AssetInventory, id: string) {
const value = config.workspaces[id];
if (!value) throw new Error("Unknown configured workspace");
return realpath(resolve(dirname(configPath), value));
}
async function* scopedFiles(root: string, deadline: number): AsyncGenerator<string> {
const pending = [{ path: root, depth: 0 }];
let count = 0;
while (pending.length) {
if (Date.now() > deadline) throw new Error("Asset inventory deadline reached");
const current = pending.pop()!;
if (current.depth > 24) throw new Error("Asset directory depth limit exceeded");
for (const item of await readdir(current.path, { withFileTypes: true })) {
if (item.isSymbolicLink() || ["node_modules", ".cache", ".temp", ".git"].includes(item.name)) continue;
const path = join(current.path, item.name);
if (protectedInput(path)) continue;
if (++count > MAX_FILES) throw new Error("Scoped asset entry limit exceeded");
if (item.isDirectory()) pending.push({ path, depth: current.depth + 1 });
else if (item.isFile()) yield path;
}
}
}
export async function collectAssets(configPath: string, appIds?: string[]): Promise<AssetPlan> {
const config = await readInventory(configPath),
objects = new Map<string, AssetObject>();
const apps: AssetPlan["apps"] = [],
deadline = Date.now() + 30_000;
for (const app of config.apps) {
if (appIds && !appIds.includes(app.id)) continue;
const root = await workspace(configPath, config, app.workspace),
refs: AssetReference[] = [];
const logical = new Set<string>();
for (const assetRoot of app.assetRoots) {
const selected = inside(root, await realpath(resolve(root, assetRoot)));
if (protectedInput(selected) || !(await lstat(selected)).isDirectory())
throw new Error("Configured asset root is not a directory");
for await (const file of scopedFiles(selected, deadline)) {
const actual = inside(selected, await realpath(file));
const metadata = await lstat(actual);
if (metadata.size > MAX_OBJECT_BYTES) throw new Error("Asset object exceeds the explicit size limit");
const digest = await identity(actual),
path = relative(root, actual).replaceAll("\\", "/");
if (logical.has(path)) continue;
logical.add(path);
const objectKey = config.storage.assets.prefix + digest.sha256;
const existing = objects.get(digest.sha256);
if (existing) {
if (existing.bytes !== digest.bytes) throw new Error("Conflicting content identity");
if (!existing.sources.includes(actual)) existing.sources.push(actual);
} else objects.set(digest.sha256, { ...digest, objectKey, sources: [actual] });
refs.push({ path, ...digest, objectKey, type: Bun.file(actual).type || "application/octet-stream" });
}
}
apps.push({ id: app.id, assets: refs.sort((a, b) => a.path.localeCompare(b.path)) });
}
if (appIds && appIds.some(id => !apps.some(app => app.id === id))) throw new Error("Unknown selected app");
return {
schemaVersion: 1,
kind: "app-asset-plan",
configSHA256: (await identity(configPath)).sha256,
storage: config.storage.assets,
objects: [...objects.values()]
.map(object => ({ ...object, sources: object.sources.sort() }))
.sort((a, b) => a.sha256.localeCompare(b.sha256)),
apps,
qualification: { uploaded: false, publicDelivery: false },
};
}
interface CloudflareDataOwner {
loadCloudflareEnvironment(file?: string): Promise<Record<string, string | undefined>>;
createR2Client(options: { bucket: string; env: Record<string, string | undefined> }): Bun.S3Client;
}
export async function r2Owner(config: AssetInventory, bucket: string): Promise<Bun.S3Client> {
const ownerPath = join(runtimeCheckout(config), "packages/bun-cloudflare/src/api.ts");
const owner = (await import(pathToFileURL(ownerPath).href)) as CloudflareDataOwner;
return owner.createR2Client({ bucket, env: await owner.loadCloudflareEnvironment() });
}
async function remoteBytes(response: Response, expected: number): Promise<Uint8Array<ArrayBuffer>> {
if (!response.ok || !response.body) throw new Error("R2 object verification failed");
const chunks: Uint8Array[] = [],
reader = response.body.getReader();
let bytes = 0;
try {
for (;;) {
const next = await reader.read();
if (next.done) break;
bytes += next.value.byteLength;
if (bytes > expected || bytes > MAX_OBJECT_BYTES) throw new Error("R2 object length exceeds its pinned identity");
chunks.push(next.value);
}
} finally {
await reader.cancel().catch(() => {});
}
if (bytes !== expected) throw new Error("R2 object length differs from its pinned identity");
const result = new Uint8Array(bytes);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
}
export async function putContentObject(
client: Bun.S3Client,
storage: AssetStorage,
path: string,
expected: { sha256: string; bytes: number },
signal: AbortSignal,
) {
if (protectedInput(path) || !/^[a-f0-9]{64}$/.test(expected.sha256) || expected.bytes > MAX_OBJECT_BYTES)
throw new Error("Invalid scoped content object input");
const digest = await identity(path);
if (digest.sha256 !== expected.sha256 || digest.bytes !== expected.bytes)
throw new Error("Content changed after planning");
const body = await Bun.file(path).bytes();
if (body.byteLength !== expected.bytes || (await contentIdentity(new Blob([body]))).sha256 !== expected.sha256)
throw new Error("Content changed while reading");
const objectKey = storage.prefix + expected.sha256,
object = client.file(objectKey);
const response = await fetch(object.presign({ method: "PUT", expiresIn: 60, type: "application/octet-stream" }), {
method: "PUT",
body,
signal,
redirect: "error",
headers: {
"If-None-Match": "*",
"Content-Type": "application/octet-stream",
"Cache-Control": "public, max-age=31536000, immutable",
},
});
await response.body?.cancel();
if (!response.ok && response.status !== 412) throw new Error("R2 conditional object write failed");
const verified = await remoteBytes(
await fetch(object.presign({ method: "GET", expiresIn: 60 }), {
signal,
redirect: "error",
}),
expected.bytes,
);
if ((await contentIdentity(new Blob([verified]))).sha256 !== expected.sha256)
throw new Error("Existing R2 object does not match its content address");
return {
...expected,
objectKey,
created: response.status !== 412,
checksumVerified: true as const,
url: storage.origin.replace(/\/$/, "") + "/" + objectKey,
};
}
export async function uploadAssets(configPath: string, planPath: string) {
const config = await readInventory(configPath),
plan = (await Bun.file(planPath).json()) as AssetPlan;
if (
plan.schemaVersion !== 1 ||
plan.kind !== "app-asset-plan" ||
plan.configSHA256 !== (await identity(configPath)).sha256 ||
JSON.stringify(plan.storage) !== JSON.stringify(config.storage.assets)
)
throw new Error("Asset plan does not match current owner configuration");
const current = await collectAssets(
configPath,
plan.apps.map(app => app.id),
);
if (
JSON.stringify(current.apps) !== JSON.stringify(plan.apps) ||
JSON.stringify(current.objects) !== JSON.stringify(plan.objects)
)
throw new Error("Scoped assets changed after planning");
const client = await r2Owner(config, config.storage.assets.bucket),
signal = AbortSignal.timeout(600_000),
objects = [];
for (const object of plan.objects)
objects.push(await putContentObject(client, plan.storage, object.sources[0]!, object, signal));
return {
schemaVersion: 1,
kind: "app-assets-upload",
objects,
appReferences: plan.apps.map(app => ({ ...app, origin: plan.storage.origin })),
qualification: { objectChecksums: true, publicDelivery: false },
catalogPublication: "owned by the edge/catalog capability",
};
}
async function main() {
const [action = "--help", ...args] = Bun.argv.slice(2);
if (action === "--help") {
console.log("bun infra/assets.ts plan|upload --config FILE --out FILE [--plan FILE]");
return;
}
const flags: Record<string, string> = {};
for (let i = 0; i < args.length; i += 2) {
const key = args[i],
value = args[i + 1];
if (!key || !["--config", "--out", "--plan"].includes(key) || !value || flags[key])
throw new Error("Invalid asset option");
flags[key] = value;
}
if (!flags["--config"] || !flags["--out"]) throw new Error("Explicit configuration and output receipt required");
const configPath = resolve(flags["--config"]),
output = resolve(flags["--out"]);
const result =
action === "plan"
? await collectAssets(configPath)
: action === "upload" && flags["--plan"]
? await uploadAssets(configPath, resolve(flags["--plan"]))
: null;
if (!result) throw new Error("Unknown asset action or missing plan");
await mkdir(dirname(output), { recursive: true });
await Bun.write(output, JSON.stringify(result, null, 2) + "\n");
console.log(JSON.stringify({ output, kind: result.kind }));
}
if (import.meta.main)
main().catch(() => {
console.error("Scoped asset operation failed; no credentials or signed URLs are logged");
process.exitCode = 1;
});
+250
View File
@@ -0,0 +1,250 @@
name: bun-control-plane
x-service: &service
restart: unless-stopped
init: true
labels:
komodo.skip: "true"
logging:
driver: json-file
options:
max-size: 10m
max-file: "1"
services:
database:
<<: *service
image: ${POSTGRES_DOCUMENTDB_IMAGE:?Resolve the immutable reference from control-plane.json}
platform: ${DEPLOYMENT_PLATFORM:?Qualify linux/amd64 or linux/arm64}
environment:
POSTGRES_DB: postgres
POSTGRES_USER_FILE: /run/secrets/database_username
POSTGRES_PASSWORD_FILE: /run/secrets/database_password
POSTGRES_HOST_AUTH_METHOD: scram-sha-256
secrets:
- database_username
- database_password
volumes:
- type: bind
source: ${DEPLOYMENT_STATE_ROOT:?Qualify an owned persistent build-disk apps directory}/postgres
target: /var/lib/postgresql/data
bind:
create_host_path: false
networks: [database]
healthcheck:
test: [CMD, pg_isready, --host, 127.0.0.1, --dbname, postgres]
interval: 15s
timeout: 5s
start_period: 30s
retries: 5
stop_grace_period: 1m
ferretdb:
<<: *service
image: ${FERRETDB_IMAGE:?Resolve the immutable reference from control-plane.json}
platform: ${DEPLOYMENT_PLATFORM:?Qualify linux/amd64 or linux/arm64}
depends_on:
database:
condition: service_healthy
environment:
FERRETDB_POSTGRESQL_URL_FILE: /run/secrets/ferretdb_postgresql_uri
FERRETDB_TELEMETRY: disabled
secrets:
- ferretdb_postgresql_uri
volumes:
- type: bind
source: ${DEPLOYMENT_STATE_ROOT:?Qualify an owned persistent build-disk apps directory}/ferretdb
target: /state
bind:
create_host_path: false
networks: [database]
security_opt: [no-new-privileges:true]
cap_drop: [ALL]
# The pinned production image provides /ferretdb ping as its native readiness probe.
stop_grace_period: 30s
core:
<<: *service
image: ${KOMODO_CORE_IMAGE:?Resolve the immutable reference from control-plane.json}
platform: ${DEPLOYMENT_PLATFORM:?Qualify linux/amd64 or linux/arm64}
depends_on:
ferretdb:
condition: service_healthy
environment:
KOMODO_HOST: ${KOMODO_PUBLIC_URL:?Resolve the deploy HTTPS route from apps.json}
KOMODO_BIND_IP: 0.0.0.0
KOMODO_DATABASE_ADDRESS: ferretdb:27017
KOMODO_DATABASE_USERNAME_FILE: /run/secrets/database_username
KOMODO_DATABASE_PASSWORD_FILE: /run/secrets/database_password
KOMODO_LOCAL_AUTH: "true"
KOMODO_INIT_ADMIN_USERNAME: ${KOMODO_ADMIN_USERNAME:?Resolve the actual owner account}
KOMODO_INIT_ADMIN_PASSWORD_FILE: /run/secrets/komodo_admin_password
KOMODO_JWT_SECRET_FILE: /run/secrets/komodo_jwt
KOMODO_WEBHOOK_SECRET_FILE: /run/secrets/komodo_webhook
KOMODO_DISABLE_USER_REGISTRATION: "true"
KOMODO_DISABLE_LOCAL_USER_REGISTRATION: "true"
KOMODO_DISABLE_OIDC_USER_REGISTRATION: "true"
KOMODO_DISABLE_INIT_RESOURCES: "true"
KOMODO_FIRST_SERVER_NAME: ${KOMODO_SERVER_NAME:?Resolve the shared server name from control-plane.json}
KOMODO_PERIPHERY_PUBLIC_KEY: file:/config/keys/periphery.pub
secrets:
- database_username
- database_password
- komodo_admin_password
- komodo_jwt
- komodo_webhook
ports:
- 127.0.0.1:${KOMODO_LOCAL_PORT:?Resolve the loopback port from the component profile}:9120
volumes:
- type: bind
source: ${DEPLOYMENT_STATE_ROOT:?Qualify an owned persistent build-disk apps directory}/keys
target: /config/keys
bind:
create_host_path: false
networks: [database, control]
healthcheck:
test: [CMD, curl, --fail, --silent, --show-error, --max-time, "4", http://127.0.0.1:9120/version]
interval: 30s
timeout: 5s
start_period: 1m
retries: 5
stop_grace_period: 30s
periphery:
<<: *service
image: ${KOMODO_PERIPHERY_IMAGE:?Resolve the immutable reference from control-plane.json}
platform: ${DEPLOYMENT_PLATFORM:?Qualify linux/amd64 or linux/arm64}
depends_on:
core:
condition: service_started
environment:
PERIPHERY_CORE_ADDRESS: ws://core:9120
PERIPHERY_CONNECT_AS: ${KOMODO_SERVER_NAME:?Resolve the shared server name from control-plane.json}
PERIPHERY_CORE_PUBLIC_KEYS: file:/config/keys/core.pub
PERIPHERY_ROOT_DIRECTORY: ${DEPLOYMENT_MANAGED_ROOT:?Qualify the owned managed apps root}
PERIPHERY_DISABLE_TERMINALS: "true"
PERIPHERY_DISABLE_CONTAINER_TERMINALS: "true"
volumes:
- type: bind
source: ${DEPLOYMENT_STATE_ROOT:?Qualify an owned persistent build-disk apps directory}/keys
target: /config/keys
bind:
create_host_path: false
- type: bind
source: ${DEPLOYMENT_DOCKER_SOCKET:?Qualify the dedicated persistent deployment daemon}
target: /var/run/docker.sock
bind:
create_host_path: false
- type: bind
source: ${DEPLOYMENT_MANAGED_ROOT:?Qualify the owned managed apps root}
target: ${DEPLOYMENT_MANAGED_ROOT:?Qualify the owned managed apps root}
bind:
create_host_path: false
- type: bind
source: /proc
target: /proc
read_only: true
bind:
create_host_path: false
networks: [control]
stop_grace_period: 30s
forgejo:
<<: *service
image: ${FORGEJO_IMAGE:?Resolve the immutable LTS reference from control-plane.json}
platform: ${DEPLOYMENT_PLATFORM:?Qualify linux/amd64 or linux/arm64}
user: 1000:1000
environment:
FORGEJO__database__DB_TYPE: sqlite3
FORGEJO__server__ROOT_URL: ${FORGEJO_PUBLIC_URL:?Resolve the git HTTPS route from apps.json}
FORGEJO__server__SSH_DOMAIN: ${FORGEJO_SSH_HOST:?Resolve the qualified SSH access host}
FORGEJO__server__SSH_PORT: ${FORGEJO_PUBLIC_SSH_PORT:?Resolve the actual qualified SSH port}
FORGEJO__server__SSH_LISTEN_PORT: "2222"
FORGEJO__server__START_SSH_SERVER: "true"
FORGEJO__security__INSTALL_LOCK: "true"
FORGEJO__service__DISABLE_REGISTRATION: "true"
FORGEJO__actions__ENABLED: "true"
FORGEJO__actions__ARTIFACT_RETENTION_DAYS: "7"
FORGEJO__actions__LOG_RETENTION_DAYS: "7"
volumes:
- type: bind
source: ${DEPLOYMENT_STATE_ROOT:?Qualify an owned persistent build-disk apps directory}/forgejo
target: /var/lib/gitea
bind:
create_host_path: false
ports:
- 127.0.0.1:${FORGEJO_LOCAL_PORT:?Resolve the loopback port from the component profile}:3000
- 127.0.0.1:${FORGEJO_LOCAL_SSH_PORT:?Resolve the loopback SSH port from the component profile}:2222
networks: [control]
healthcheck:
test: [CMD, curl, --fail, --silent, --show-error, --max-time, "4", http://127.0.0.1:3000/api/healthz]
interval: 30s
timeout: 5s
start_period: 1m
retries: 5
stop_grace_period: 1m
security_opt: [no-new-privileges:true]
cap_drop: [ALL]
runner:
<<: *service
profiles: [ci]
image: ${FORGEJO_RUNNER_IMAGE:?Resolve the immutable reference from control-plane.json}
platform: ${DEPLOYMENT_PLATFORM:?Qualify linux/amd64 or linux/arm64}
user: 1000:1000
working_dir: /data
entrypoint: [forgejo-runner]
command: [daemon, --config, /etc/forgejo-runner/config.yaml]
depends_on:
forgejo:
condition: service_healthy
environment:
DOCKER_HOST: unix:///var/run/docker.sock
group_add:
- ${CI_DOCKER_SOCKET_GID:-1000}
secrets:
- forgejo_runner_token
configs:
- source: runner_config
target: /etc/forgejo-runner/config.yaml
volumes:
- type: bind
source: ${DEPLOYMENT_STATE_ROOT:?Qualify an owned persistent build-disk apps directory}/runner
target: /data
bind:
create_host_path: false
- type: bind
source: ${CI_DOCKER_SOCKET:-${DEPLOYMENT_STATE_ROOT:?Qualify the owned state root}/ci-engine/run/docker.sock}
target: /var/run/docker.sock
bind:
create_host_path: false
networks: [control]
stop_grace_period: 1m
security_opt: [no-new-privileges:true]
cap_drop: [ALL]
networks:
database:
internal: true
control: {}
secrets:
database_username:
file: ${DEPLOYMENT_DATABASE_USERNAME_FILE:?Supply the isolated credential-owner file}
database_password:
file: ${DEPLOYMENT_DATABASE_PASSWORD_FILE:?Supply the isolated credential-owner file}
ferretdb_postgresql_uri:
file: ${FERRETDB_POSTGRESQL_URI_FILE:?Supply the isolated encoded PostgreSQL URI file}
komodo_admin_password:
file: ${KOMODO_ADMIN_PASSWORD_FILE:?Supply the actual isolated bootstrap credential file}
komodo_jwt:
file: ${KOMODO_JWT_SECRET_FILE:?Supply the isolated persistent JWT secret file}
komodo_webhook:
file: ${KOMODO_WEBHOOK_SECRET_FILE:?Supply the isolated webhook secret file}
forgejo_runner_token:
file: ${FORGEJO_RUNNER_TOKEN_FILE:-${DEPLOYMENT_SECRETS_ROOT:?Qualify the isolated secret-owner root}/forgejo-runner-token}
configs:
runner_config:
# Render runner.configuration from control-plane.json only after registration/job-image qualification.
file: ${FORGEJO_RUNNER_CONFIG_FILE:-${DEPLOYMENT_STATE_ROOT:?Qualify the owned state root}/runner/config.yaml}
+192
View File
@@ -0,0 +1,192 @@
{
"schemaVersion": 1,
"kind": "deployment-component-profile",
"catalog": "./apps.json",
"compose": "./compose.yaml",
"status": "prepared-not-deployed",
"verifiedAt": "2026-10-13",
"owners": {
"desiredState": "./apps.json",
"deploymentExecutor": "Komodo Core and Periphery",
"sourceAndWorkflows": "Forgejo and Forgejo Runner",
"appBuilds": "Existing native Bun app and factory owners",
"sourceExports": "../factory/source-export.ts",
"edge": "Cloudflare MCP and the existing Bun edge adapter",
"sharedAssets": "The content-addressed R2 owner referenced by ./apps.json"
},
"images": {
"komodo-core": {
"environment": "KOMODO_CORE_IMAGE",
"reference": "ghcr.io/moghtech/komodo-core:2.3.3@sha256:bca73d0eee143066228fb9b80c38a5a2726e573ad2758f7aa92b5c91a8aef1c7",
"release": "2.3.3",
"sourceRevision": "780ac68b992094a9fccd5fffb760e0c84fd3c3d1",
"source": "https://github.com/moghtech/komodo/releases/tag/v2.3.3",
"platforms": ["linux/amd64", "linux/arm64"]
},
"komodo-periphery": {
"environment": "KOMODO_PERIPHERY_IMAGE",
"reference": "ghcr.io/moghtech/komodo-periphery:2.3.3@sha256:fa3f1a641a265216066676950d5eecdf506955a7bafe2f01996135ae93115cd5",
"release": "2.3.3",
"sourceRevision": "780ac68b992094a9fccd5fffb760e0c84fd3c3d1",
"source": "https://github.com/moghtech/komodo/releases/tag/v2.3.3",
"platforms": ["linux/amd64", "linux/arm64"]
},
"ferretdb": {
"environment": "FERRETDB_IMAGE",
"reference": "ghcr.io/ferretdb/ferretdb:2.7.0@sha256:5706414241eb84f0515512c37b46db0f1b1eac9e5ceb7e4c2523211c184b1985",
"release": "2.7.0",
"source": "https://github.com/FerretDB/FerretDB/releases/tag/v2.7.0",
"platforms": ["linux/amd64", "linux/arm64"],
"stateOwner": "1000:1000"
},
"postgres-documentdb": {
"environment": "POSTGRES_DOCUMENTDB_IMAGE",
"reference": "ghcr.io/ferretdb/postgres-documentdb:17-0.107.0-ferretdb-2.7.0@sha256:2386795ec2aa7ae559304361979f1dc5708d383ee9020ae63dadc2940dfe58f7",
"release": "0.107.0-ferretdb-2.7.0",
"postgresVersion": "17.6",
"source": "https://github.com/FerretDB/documentdb/releases/tag/v0.107.0-ferretdb-2.7.0",
"platforms": ["linux/amd64", "linux/arm64"],
"stateOwner": "999:999"
},
"forgejo": {
"environment": "FORGEJO_IMAGE",
"reference": "codeberg.org/forgejo/forgejo:15.0.10-rootless@sha256:6ffebd9ab9ee9f6677e3523fa60debd9de630ff2a226fa681d6cc0c7ba541d77",
"release": "15.0.10",
"support": "LTS",
"supportEnds": "2027-07-15",
"source": "https://forgejo.org/releases/",
"platforms": ["linux/amd64", "linux/arm64", "linux/arm/v6"],
"stateOwner": "1000:1000"
},
"forgejo-runner": {
"environment": "FORGEJO_RUNNER_IMAGE",
"reference": "code.forgejo.org/forgejo/runner:13.2.0@sha256:ca3d5eea46004789a175d1369eec6829d3ea9bfbe2011a06625b4bdaa55f7552",
"release": "13.2.0",
"source": "https://forgejo.org/2026-08-runner-release-v13/",
"platforms": ["linux/amd64", "linux/arm64"],
"stateOwner": "1000:1000"
}
},
"database": {
"backend": "FerretDB 2 with PostgreSQL DocumentDB",
"components": ["ferretdb", "postgres-documentdb"],
"contract": "https://github.com/moghtech/komodo/blob/v2.3.3/compose/ferretdb.compose.yaml",
"authentication": "SCRAM-SHA-256",
"credentialOwner": "Isolated runtime secret files",
"freshInstance": true,
"reuseExistingDatabase": false,
"mongodbExcluded": {
"reason": "The observed host kernel is in MongoDB's unsupported 6.19 through 7.0.13 range",
"contract": "https://www.mongodb.com/docs/manual/release-notes/8.0/"
}
},
"execution": {
"controlLanguage": "Rust",
"firstServerName": "managed-apps",
"firstServerNameEnvironment": "KOMODO_SERVER_NAME",
"deploymentContextEnvironment": "DEPLOYMENT_DOCKER_CONTEXT",
"deploymentSocketEnvironment": "DEPLOYMENT_DOCKER_SOCKET",
"runnerSocketEnvironment": "CI_DOCKER_SOCKET",
"stateRootEnvironment": "DEPLOYMENT_STATE_ROOT",
"managedAppRootEnvironment": "DEPLOYMENT_MANAGED_ROOT",
"stateLocation": "Explicitly qualified persistent subdirectory of the host's build-disk ~/apps root",
"separatePersistentDeploymentDaemon": true,
"separateRunnerDaemon": true,
"reuseFactoryDaemon": false,
"reuseGlobalDaemon": false,
"publicAgentListener": false,
"defaultGlobalUpdateOrBackupProcedures": false,
"sourceAuthority": "Existing GitHub fork ownership and histories remain canonical",
"providerAuthenticationSynchronization": false
},
"routes": {
"owner": "./apps.json",
"controlRole": "deploy",
"forgeRole": "git",
"actionsRole": "ci",
"komodoLoopbackPort": 19120,
"forgejoLoopbackPort": 19300,
"forgejoSshLoopbackPort": 19222,
"runnerHasHttpService": false,
"actionsRouteBackend": "Forgejo Actions UI"
},
"runner": {
"composeProfile": "ci",
"minimumDockerVersion": "25.0.0",
"ipv6DockerFixVersion": "28.0.1",
"configFileEnvironment": "FORGEJO_RUNNER_CONFIG_FILE",
"configuration": {
"log": { "level": "info", "job_level": "info" },
"runner": {
"capacity": 1,
"timeout": "45m",
"shutdown_timeout": "1m",
"fetch_timeout": "30s",
"fetch_interval": "30s",
"labels": ["bun:docker://${BUN_JOB_IMAGE}"]
},
"cache": { "enabled": false },
"container": {
"privileged": false,
"valid_volumes": [],
"docker_host": "-",
"force_pull": false,
"force_rebuild": false
},
"server": {
"connections": {
"aphrody": {
"url": "${FORGEJO_PUBLIC_URL}",
"uuid": "${FORGEJO_RUNNER_UUID}",
"token_url": "file:/run/secrets/forgejo_runner_token"
}
}
}
},
"jobImageEnvironment": "BUN_JOB_IMAGE",
"jobImageRequirement": "Immutable OCI reference with a qualified exact Aphrody Bun runtime and required native build owners",
"javascriptActionsCompatibility": "Not qualified; Bun shell workflows do not imply Node action compatibility",
"registration": "Forgejo UI/API UUID and isolated token file; no deprecated GITEA variables"
},
"secrets": {
"root": "DEPLOYMENT_SECRETS_ROOT",
"databaseUsername": "DEPLOYMENT_DATABASE_USERNAME_FILE",
"databasePassword": "DEPLOYMENT_DATABASE_PASSWORD_FILE",
"ferretdbPostgresqlUri": "FERRETDB_POSTGRESQL_URI_FILE",
"komodoBootstrapPassword": "KOMODO_ADMIN_PASSWORD_FILE",
"komodoJwt": "KOMODO_JWT_SECRET_FILE",
"komodoWebhook": "KOMODO_WEBHOOK_SECRET_FILE",
"forgejoRunnerToken": "FORGEJO_RUNNER_TOKEN_FILE",
"valuesInCatalog": false
},
"qualification": {
"ociIndexDigestAndManifestBytesVerified": true,
"imageLayersPulled": false,
"liveServiceExecution": false,
"runtimeBackendCompatibility": false,
"persistentDockerContextsQualified": false,
"forgeAndRunnerRegistered": false,
"cloudflareHttpsRouting": false,
"factoryOverlapAllowed": false,
"observedHost": {
"target": "vps",
"observedAt": "2026-10-13T10:13:45.389Z",
"architecture": "x64",
"kernel": "7.0.0-38-generic",
"avx": true,
"rootFreeBytes": 133501100032,
"buildFreeBytes": 127031021568,
"buildFreeInodes": 17583894
},
"activationGates": [
"Factory heavy jobs have terminal receipts and the shared heavy lock is available",
"Resolve one desired-state catalog and fresh isolated persistent data paths",
"Qualify distinct persistent deployment and CI Docker contexts, sockets and data roots",
"Verify image layers, native platform, executable identities and directory/secret ownership",
"Provide actual secret files, first admin account and registered runner UUID/token",
"Qualify PostgreSQL, DocumentDB, FerretDB, Komodo and authenticated Periphery readiness",
"Qualify Forgejo health, Git SSH/HTTPS and Bun job execution",
"Activate public Cloudflare routes only after the authenticated origin gates pass"
]
}
}
+131
View File
@@ -0,0 +1,131 @@
// SPDX-License-Identifier: MIT
import { chmod, link, lstat, mkdir, unlink } from "node:fs/promises";
import { delimiter, join } from "node:path";
import { contentIdentity, identity } from "../src/distribution.ts";
import type { NativeLaunchContract } from "./app-build.ts";
declare const __BUN_OS_NATIVE_CONTRACT__: NativeLaunchContract;
async function materialize(contract: NativeLaunchContract) {
const home = Bun.env.USERPROFILE ?? Bun.env.HOME;
const base =
process.platform === "win32"
? (Bun.env.LOCALAPPDATA ?? (home ? join(home, "AppData/Local") : undefined))
: (Bun.env.XDG_CACHE_HOME ?? (home ? join(home, ".cache") : undefined));
if (!base) throw new Error("Native payload requires a configured user cache directory");
const root = join(base, "bun-os/native"),
objects = join(root, "objects/sha256");
const launchIdentity = (await contentIdentity(new Blob([JSON.stringify(contract)]))).sha256;
const launch = join(root, "launch", launchIdentity);
await mkdir(objects, { recursive: true, mode: 0o700 });
await mkdir(launch, { recursive: true, mode: 0o700 });
const destinations = [];
for (const file of contract.files) {
if (
!/^[a-f0-9]{64}$/.test(file.sha256) ||
!/^[a-zA-Z0-9_.+-]+$/.test(file.name) ||
file.bytes < 1 ||
file.bytes > 256 * 1024 * 1024
)
throw new Error("Invalid pinned native payload contract");
const object = join(objects, file.sha256);
if (!(await Bun.file(object).exists())) {
let embedded: Blob | undefined;
for (const candidate of Bun.embeddedFiles) {
if (candidate.size !== file.bytes) continue;
if ((await contentIdentity(candidate)).sha256 === file.sha256) {
embedded = candidate;
break;
}
}
if (!embedded) throw new Error("Qualified native payload is absent from the standalone binary");
const temporary = join(objects, "." + file.sha256 + "-" + crypto.randomUUID());
await Bun.write(temporary, embedded);
if ((await identity(temporary)).sha256 !== file.sha256)
throw new Error("Materialized native payload checksum mismatch");
await chmod(temporary, file.role === "executable" ? 0o700 : 0o600);
try {
await link(temporary, object);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
} finally {
await unlink(temporary);
}
}
const proof = await identity(object);
if (proof.sha256 !== file.sha256 || proof.bytes !== file.bytes)
throw new Error("Native object cache integrity failed");
if (file.role === "executable") await chmod(object, 0o700);
const destination = join(launch, file.name);
try {
await link(object, destination);
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "EEXIST") throw error;
}
const metadata = await lstat(destination);
if (!metadata.isFile() || metadata.isSymbolicLink() || (await identity(destination)).sha256 !== file.sha256)
throw new Error("Native loader view integrity failed");
destinations.push({ ...file, destination });
}
const backend = destinations.find(file => file.kind === "backend");
if (!backend) throw new Error("Native backend has not been qualified");
if (process.platform === "win32" && !backend.name.toLowerCase().endsWith(".exe"))
throw new Error("Windows backend needs its actual executable filename");
return { backend: backend.destination, directory: launch };
}
async function main() {
if (typeof __BUN_OS_NATIVE_CONTRACT__ === "undefined" || !Bun.isStandaloneExecutable)
throw new Error("Build this native app through its configured Bun application owner");
const contract = __BUN_OS_NATIVE_CONTRACT__;
const actual =
process.platform === "win32"
? "windows-" + process.arch
: process.platform === "darwin"
? "macos-" + process.arch
: process.platform + "-" + process.arch;
if (actual !== contract.target) throw new Error("Native app target does not match this execution context");
if (Bun.argv.slice(2).length === 1 && Bun.argv[2] === "--bun-os-runtime-provenance") {
console.log(
JSON.stringify({
appId: contract.appId,
target: contract.target,
version: Bun.version,
revision: Bun.revision,
platform: process.platform,
architecture: process.arch,
nativePayloads: contract.files.map(({ name, sha256, bytes }) => ({ name, sha256, bytes })),
}),
);
return;
}
const selected = await materialize(contract);
const child = Bun.spawn([selected.backend, ...Bun.argv.slice(2)], {
cwd: process.cwd(),
env: {
...Bun.env,
PATH: selected.directory + delimiter + (Bun.env.PATH ?? ""),
...(process.platform === "linux"
? { LD_LIBRARY_PATH: selected.directory + delimiter + (Bun.env.LD_LIBRARY_PATH ?? "") }
: {}),
},
stdin: "inherit",
stdout: "inherit",
stderr: "inherit",
windowsHide: true,
});
const interrupt = () => child.kill("SIGINT"),
terminate = () => child.kill("SIGTERM");
process.on("SIGINT", interrupt);
process.on("SIGTERM", terminate);
try {
process.exitCode = await child.exited;
} finally {
process.off("SIGINT", interrupt);
process.off("SIGTERM", terminate);
}
}
if (import.meta.main)
main().catch(error => {
console.error(error instanceof Error ? error.message : "Native app failed");
process.exitCode = 1;
});
+35
View File
@@ -0,0 +1,35 @@
{
"name": "@aphrody/bun-os",
"version": "1.4.4",
"private": true,
"description": "Bun OS source assembly and full/perf distribution factory",
"license": "MIT",
"bin": {
"bun-os": "src/cli.ts"
},
"type": "module",
"exports": {
".": "./src/distribution.ts"
},
"scripts": {
"plan": "bun src/cli.ts plan",
"sources": "bun src/cli.ts sources",
"stage": "bun src/cli.ts stage",
"wasm": "bun src/cli.ts wasm",
"image": "bun src/cli.ts image",
"typecheck": "bun check -p tsconfig.json --threads=2 --no-pretty",
"lint:all": "bun lint --deny-warnings src container factory infra packages/package-converter/src",
"fmt:all": "bun fmt --write -c bun-format.json src container factory infra packages/package-converter/src",
"fmt:check": "bun fmt --check -c bun-format.json src container factory infra packages/package-converter/src",
"migrate": "bun n2b . --aggressive --ignore 'node_modules/**' --ignore 'out/**' --ignore '.git/**' --jobs 2 --agent --report json",
"factory": "bun factory/ubuntu.ts",
"packages": "bun packages/package-converter/src/cli.ts",
"apt:build": "bun src/build-apt.ts"
},
"devDependencies": {
"@aphrody/bun-types": "1.4.4"
},
"engines": {
"bun": ">=1.4.4"
}
}
+66
View File
@@ -0,0 +1,66 @@
# Package converter
Read Debian, Ubuntu and COSMIC package metadata and `.deb` archives, ALPM/MSYS2
`.PKGINFO`, `.SRCINFO` and package archives, Cygwin `setup.ini`, and package source
archives. Dependency expressions, versions, source checksums and payload ABI stay
associated with their original ecosystem.
```powershell
bun packages/package-converter/src/cli.ts inspect app.deb --format deb --ecosystem ubuntu --decoder 7z
bun packages/package-converter/src/cli.ts convert Sources --format deb822 --ecosystem debian --package app --out out/app-port --source-base https://deb.debian.org/debian/
bun packages/package-converter/src/cli.ts convert app.src.tar.gz --format source-archive --ecosystem msys2 --package app --out out/app-source --extract-sources
```
For archives without embedded package metadata, supply a companion, for example
`--metadata setup.ini --metadata-format cygwin` or
`--metadata Sources --metadata-format deb822`. Matching archive names are checked
against the companion's declared size and checksum; both input hashes are retained.
Conversion preserves an existing native PE archive as a private Bun package:
DLLs, headers, static/import libraries, data and license files retain their payload
paths and hashes. `native-assets.json` records PE architecture, imports, bundled DLL
paths, external runtime owners and the original dependencies. DLL packages expose
library and asset paths for application loading. Native assets are exported through
`./native-assets` and `./payload/*`; runtime loading remains with the application.
```powershell
bun packages/package-converter/src/cli.ts convert mingw-w64-ucrt-x86_64-zlib.pkg.tar.zst --format alpm-archive --ecosystem msys2 --package mingw-w64-ucrt-x86_64-zlib --out out/zlib-native
```
Metadata, source contracts and foreign ELF inputs produce a Windows port job.
Supply the package owner's actual rebuilt `.exe` to create a private Bun binary
package and, optionally, a WinGet manifest:
```powershell
bun packages/package-converter/src/cli.ts convert .PKGINFO --format pkginfo --ecosystem mingw --package app --out out/app-windows --windows-artifact app.exe --winget-id Example.App --installer-url https://example.org/app.exe --publisher Example --license MIT
```
The converter reads executable headers and imported DLLs. Non-system imports
require explicit `--runtime DLL=Publisher.Package` mappings for WinGet owners or
`--runtime DLL=bun:@scope/package@1.2.3` for a pinned Bun dependency. Bundled DLLs
retain their own import requirements; Cygwin, MSYS2 and VC redistributables are
external runtimes unless present in the payload. WinGet requires a real user-mode
PE executable and WinGet runtime owners; a DLL is never rendered as an installer.
WinGet owner mappings are recorded as system prerequisites; Bun-owned runtime
packages become pinned `package.json` dependencies.
Linux ELF payloads produce Windows rebuild jobs. Existing output directories are refused. Metadata
checksums retain their algorithm; repository signatures belong to native APT,
pacman or Cygwin and are not verified by metadata conversion.
For source versions containing Debian epochs, select `--windows-version` explicitly;
the original version remains in the Bun package contract.
Gzip uses bounded decompression streams. Zstandard uses Bun's native decoder after
checking the declared size, window size, every block's maximum output and the exact
frame boundary. A frame without a declared size is accepted only when the sum of
every block's worst-case output and its window are within the 256 MiB limit.
Dictionary-dependent, skippable, multiple and trailing frames are rejected.
XZ and bzip2 use an
explicitly configured native 7-Zip executable through stdin/stdout, with a
30-second deadline. Source extraction preserves links only within the new source
tree and fails when the host cannot represent a source path or link.
Format references: [Debian binary archives](https://manpages.debian.org/unstable/dpkg-dev/deb.5.en.html),
[PKGINFO](https://man.archlinux.org/man/PKGINFO.5.en),
[SRCINFO](https://man.archlinux.org/man/SRCINFO.5.en),
[Cygwin package hints](https://cygwin.com/packaging-hint-files.html),
[WinGet manifests](https://learn.microsoft.com/en-us/windows/package-manager/package/manifest).
+17
View File
@@ -0,0 +1,17 @@
{
"name": "@bun-os/package-converter",
"version": "1.4.4",
"private": true,
"license": "MIT",
"bin": {
"package-converter": "src/cli.ts"
},
"type": "module",
"exports": {
".": "./src/convert.ts",
"./metadata": "./src/metadata.ts"
},
"engines": {
"bun": ">=1.4.4"
}
}
+124
View File
@@ -0,0 +1,124 @@
// SPDX-License-Identifier: MIT
import { type Abi } from "./types.ts";
/** Inspect actual executable headers, including PE import DLLs; extensions are not ABI evidence. */
export function abi(data: Uint8Array): Abi {
const view = new DataView(data.buffer, data.byteOffset, data.byteLength);
if (data.length >= 20 && data[0] === 0x7f && data[1] === 69 && data[2] === 76 && data[3] === 70) {
if (![1, 2].includes(data[4]!) || ![1, 2].includes(data[5]!)) throw new Error("Invalid ELF class or endianness");
if (data.length < (data[4] === 1 ? 52 : 64) || data[6] !== 1)
throw new Error("Truncated ELF header or unsupported version");
const machine = view.getUint16(18, data[5] === 1);
return {
format: "elf",
bits: data[4] === 1 ? 32 : 64,
runtime: data[7] === 3 ? "linux" : undefined,
elfOsAbi: data[7],
architecture:
({ 3: "x86", 62: "x64", 40: "arm", 183: "arm64", 243: "riscv" } as Record<number, string>)[machine] ??
`elf-${machine}`,
};
}
if (data.length >= 64 && data[0] === 77 && data[1] === 90) {
const pe = view.getUint32(60, true);
if (pe > data.length - 24 || view.getUint32(pe, true) !== 0x4550) throw new Error("Invalid PE signature");
const machine = view.getUint16(pe + 4, true);
const count = view.getUint16(pe + 6, true);
const characteristics = view.getUint16(pe + 22, true);
const optionalSize = view.getUint16(pe + 20, true);
const optional = pe + 24;
if (optionalSize < 96 || optionalSize > data.length - optional || count > 4096)
throw new Error("Truncated PE optional header");
const magic = view.getUint16(optional, true);
if (![0x10b, 0x20b].includes(magic)) throw new Error("Unsupported PE optional header");
if (
([0x14c, 0x1c4].includes(machine) && magic !== 0x10b) ||
([0x8664, 0xaa64].includes(machine) && magic !== 0x20b)
)
throw new Error("PE machine and optional-header class disagree");
if (magic === 0x20b && optionalSize < 112) throw new Error("Truncated PE32+ optional header");
const directories = optional + (magic === 0x20b ? 112 : 96);
const directoryCountOffset = optional + (magic === 0x20b ? 108 : 92);
const sections = optional + optionalSize;
if (sections + count * 40 > data.length) throw new Error("Truncated PE section table");
const rvaOffset = (rva: number): number => {
for (let i = 0; i < count; i++) {
const section = sections + i * 40;
const address = view.getUint32(section + 12, true);
const rawSize = view.getUint32(section + 16, true);
const raw = view.getUint32(section + 20, true);
if (rva >= address && rva - address < rawSize) {
const offset = raw + rva - address;
if (offset < data.length) return offset;
}
}
throw new Error("PE RVA does not map to file bytes");
};
const imports: string[] = [];
const importName = (rva: number): string => {
const offset = rvaOffset(rva);
let end = offset;
while (end < data.length && end - offset < 1024 && data[end] !== 0) end++;
if (end >= data.length || end - offset >= 1024) throw new Error("Unterminated PE import name");
const name = new TextDecoder("ascii", { fatal: true }).decode(data.subarray(offset, end)).toLowerCase();
if (!/^[a-z0-9_.+-]+$/.test(name)) throw new Error("Invalid PE import name");
return name;
};
if (view.getUint32(directoryCountOffset, true) > 1) {
if (directories + 16 > sections) throw new Error("Truncated PE import directory");
const importRva = view.getUint32(directories + 8, true);
const importSize = view.getUint32(directories + 12, true);
if (importRva) {
const start = rvaOffset(importRva);
let ended = false;
for (let i = 0; i < 4096; i++) {
const entry = start + i * 20;
if (entry + 20 > data.length || (importSize && i * 20 >= importSize))
throw new Error("Truncated PE import descriptors");
if (data.subarray(entry, entry + 20).every(byte => !byte)) {
ended = true;
break;
}
imports.push(importName(view.getUint32(entry + 12, true)));
}
if (!ended) throw new Error("PE import descriptor count exceeds limit");
}
}
if (view.getUint32(directoryCountOffset, true) > 13) {
if (directories + 112 > sections) throw new Error("Truncated PE delay import directory");
const delayRva = view.getUint32(directories + 104, true);
const delaySize = view.getUint32(directories + 108, true);
if (delayRva) {
const start = rvaOffset(delayRva);
let ended = false;
for (let i = 0; i < 4096; i++) {
const entry = start + i * 32;
if (entry + 32 > data.length || (delaySize && i * 32 >= delaySize))
throw new Error("Truncated PE delay import descriptors");
if (data.subarray(entry, entry + 32).every(byte => !byte)) {
ended = true;
break;
}
const flags = view.getUint32(entry, true);
if (flags !== 1) throw new Error("PE delay imports need RVA-based descriptors");
imports.push(importName(view.getUint32(entry + 4, true)));
}
if (!ended) throw new Error("PE delay import count exceeds limit");
}
}
return {
format: "pe",
bits: magic === 0x20b ? 64 : 32,
architecture:
({ 0x14c: "x86", 0x8664: "x64", 0x1c4: "arm", 0xaa64: "arm64" } as Record<number, string>)[machine] ??
`pe-${machine}`,
runtime: imports.includes("cygwin1.dll") ? "cygwin" : imports.includes("msys-2.0.dll") ? "msys2" : "windows",
executable: !!(characteristics & 0x2) && !(characteristics & 0x2000),
library: !!(characteristics & 0x2) && !!(characteristics & 0x2000),
subsystem: view.getUint16(optional + 68, true),
imports: [...new Set(imports)],
};
}
if (data[0] === 35 && data[1] === 33) return { format: "script" };
return { format: "data" };
}
+92
View File
@@ -0,0 +1,92 @@
// SPDX-License-Identifier: MIT
import { decoder, LIMITS, type Ecosystem, type Fields, type PackageRecord } from "./types.ts";
import { required } from "./deb822.ts";
function assignments(data: Uint8Array): Array<[string, string]> {
if (data.length > LIMITS.metadata) throw new Error("ALPM metadata exceeds limit");
return decoder
.decode(data)
.split(/\r?\n/)
.filter(line => line.trim() && !line.trimStart().startsWith("#"))
.map(line => {
if (line.length > 65536) throw new Error("ALPM line exceeds limit");
const match = /^\s*([a-z][a-z0-9_]*)\s*=\s*(.*)$/.exec(line);
if (!match) throw new Error("Invalid ALPM assignment");
return [match[1]!, match[2]!.trimEnd()];
});
}
function append(fields: Fields, key: string, value: string) {
(fields[key] ??= []).push(value);
}
function record(fields: Fields, ecosystem: Ecosystem, source: boolean): PackageRecord {
const version = source
? `${fields.epoch?.[0] ? `${fields.epoch[0]}:` : ""}${required(fields, "pkgver")}-${required(fields, "pkgrel")}`
: required(fields, "pkgver");
const dependencies: Record<string, string[]> = Object.create(null);
for (const [key, values] of Object.entries(fields)) {
if (/^(depend|optdepend|makedepend|checkdepend|provides|conflict|replaces)(_|$)/.test(key))
dependencies[key] = values;
}
const sources = Object.entries(fields)
.filter(([key]) => /^source(_|$)/.test(key))
.flatMap(([key, values]) => {
const suffix = key.slice(6);
const checksums = fields[`sha256sums${suffix}`] ?? [];
if (checksums.length && checksums.length !== values.length)
throw new Error("ALPM source/checksum count mismatch");
return values.map((value, index) => {
const separator = value.indexOf("::");
const url = separator < 0 ? value : value.slice(separator + 2);
const path = separator < 0 ? value.split("/").at(-1)! : value.slice(0, separator);
const hash = checksums[index];
if (hash && hash !== "SKIP" && !/^[a-fA-F0-9]{64}$/.test(hash)) throw new Error("Invalid ALPM SHA256");
return {
path,
url,
integrity: hash && hash !== "SKIP" ? `sha256:${hash.toLowerCase()}` : undefined,
};
});
});
return {
ecosystem,
kind: source ? "source" : "binary",
name: required(fields, "pkgname"),
version,
architecture: fields.arch?.join(" "),
description: fields.pkgdesc?.[0],
homepage: fields.url?.[0],
licenses: fields.license ?? [],
dependencies,
sources,
fields,
};
}
export function pkginfo(data: Uint8Array, ecosystem: Ecosystem): PackageRecord[] {
const fields: Fields = Object.create(null);
for (const [key, value] of assignments(data)) append(fields, key, value);
for (const key of ["pkgname", "pkgver", "arch"]) {
if (fields[key]?.length !== 1) throw new Error(`ALPM needs exactly one ${key}`);
}
return [record(fields, ecosystem, false)];
}
/** .SRCINFO is parsed without executing PKGBUILD or its shell functions. */
export function srcinfo(data: Uint8Array, ecosystem: Ecosystem): PackageRecord[] {
const base: Fields = Object.create(null);
const packages: Fields[] = [];
let current = base;
for (const [key, value] of assignments(data)) {
if (key === "pkgbase" && Object.keys(base).length) throw new Error("Repeated ALPM base section");
if (key === "pkgname") {
current = Object.create(null);
packages.push(current);
}
append(current, key, value);
}
required(base, "pkgbase");
if (!packages.length) throw new Error("ALPM source has no package sections");
return packages.map(fields => record({ ...base, ...fields }, ecosystem, true));
}
+39
View File
@@ -0,0 +1,39 @@
// SPDX-License-Identifier: MIT
import { decoder, LIMITS } from "./types.ts";
export interface ArMember {
name: string;
data: Uint8Array;
}
/** Debian uses the common ar format, decimal sizes and even-byte member alignment. */
export function ar(data: Uint8Array): ArMember[] {
if (data.length > LIMITS.packed || decoder.decode(data.subarray(0, 8)) !== "!<arch>\n")
throw new Error("Invalid or oversized ar archive");
const members: ArMember[] = [];
const names = new Set<string>();
for (let offset = 8; offset < data.length;) {
if (offset + 60 > data.length) throw new Error("Truncated ar header");
const header = decoder.decode(data.subarray(offset, offset + 60));
const name = header.slice(0, 16).trim().replace(/\/$/, "");
const sizeText = header.slice(48, 58).trim();
if (
!name ||
name.startsWith("/") ||
name.startsWith("#1/") ||
names.has(name) ||
header.slice(58) !== "`\n" ||
!/^\d+$/.test(sizeText)
)
throw new Error("Unsupported or duplicate ar member");
const size = Number(sizeText);
const start = offset + 60;
if (!Number.isSafeInteger(size) || size > data.length - start) throw new Error("Truncated ar member");
names.add(name);
members.push({ name, data: data.subarray(start, start + size) });
if (members.length > 1000) throw new Error("Too many ar members");
offset = start + size + (size % 2);
if (offset > data.length) throw new Error("Missing ar alignment byte");
}
return members;
}
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env bun
// SPDX-License-Identifier: MIT
import { convert, type ConversionOptions } from "./convert.ts";
import { inspect } from "./metadata.ts";
import { type Ecosystem, type InputFormat } from "./types.ts";
const USAGE = `package-converter inspect|convert INPUT --format FORMAT --ecosystem ECOSYSTEM
Formats: deb, deb822, pkginfo, srcinfo, cygwin, alpm-archive, source-archive
Ecosystems: debian, ubuntu, cosmic, alpm, msys2, mingw, cygwin
Convert: --package NAME --out NEW_DIRECTORY [--source-base URL] [--extract-sources]
Source archive companion: --metadata FILE --metadata-format deb822|pkginfo|srcinfo|cygwin
Windows: --windows-artifact FILE.exe [--winget-id Publisher.Package --windows-version VERSION --installer-url HTTPS_URL --publisher NAME --license SPDX]
Runtime imports: --runtime DLL=Publisher.Package or DLL=bun:package@VERSION (repeatable)
Compression: --decoder NATIVE_7ZIP_EXECUTABLE for xz or bzip2 members`;
export async function runPackageCLI(args: string[] = Bun.argv.slice(2)): Promise<void> {
args = [...args];
if (!args.length || args.includes("--help")) {
console.log(USAGE);
return;
}
const command = args.shift();
const input = args.shift();
if (!input || !["inspect", "convert"].includes(command ?? "")) throw new Error(USAGE);
const values: Record<string, string> = Object.create(null);
const runtimeDependencies: Record<string, string> = Object.create(null);
let extractSources = false;
const allowed = new Set([
"format",
"ecosystem",
"out",
"package",
"source-base",
"windows-artifact",
"winget-id",
"windows-version",
"installer-url",
"publisher",
"license",
"decoder",
"metadata",
"metadata-format",
]);
while (args.length) {
const flag = args.shift()!;
if (flag === "--extract-sources") {
extractSources = true;
continue;
}
const key = flag.startsWith("--") ? flag.slice(2) : "";
const value = args.shift();
if (!value || value.startsWith("--") || (!allowed.has(key) && key !== "runtime"))
throw new Error(`Invalid option ${flag}`);
if (key === "runtime") {
const equal = value.indexOf("=");
const dll = value.slice(0, equal).toLowerCase();
if (equal < 1 || !/^[a-z0-9_.+-]+\.dll$/.test(dll) || runtimeDependencies[dll])
throw new Error("Runtime mapping must be DLL=Publisher.Package once per DLL");
runtimeDependencies[dll] = value.slice(equal + 1);
} else {
if (values[key]) throw new Error(`Duplicate option ${flag}`);
values[key] = value;
}
}
const formats: InputFormat[] = ["deb", "deb822", "pkginfo", "srcinfo", "cygwin", "alpm-archive", "source-archive"];
const ecosystems: Ecosystem[] = ["debian", "ubuntu", "cosmic", "alpm", "msys2", "mingw", "cygwin"];
if (!formats.includes(values.format as InputFormat) || !ecosystems.includes(values.ecosystem as Ecosystem))
throw new Error(USAGE);
const options: ConversionOptions = {
format: values.format as InputFormat,
ecosystem: values.ecosystem as Ecosystem,
out: values.out ?? "",
package: values.package ?? "",
sourceBase: values["source-base"],
extractSources,
windowsArtifact: values["windows-artifact"],
wingetId: values["winget-id"],
installerUrl: values["installer-url"],
publisher: values.publisher,
license: values.license,
windowsVersion: values["windows-version"],
decoder: values.decoder,
runtimeDependencies,
metadataFile: values.metadata,
metadataFormat: values["metadata-format"] as InputFormat | undefined,
};
if (command === "inspect") console.log(JSON.stringify((await inspect(input, options)).inspection, null, 2));
else {
if (!options.out || !options.package) throw new Error("Conversion requires --out and --package");
console.log(await convert(input, options));
}
}
if (import.meta.main)
runPackageCLI().catch(error => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: MIT
import { LIMITS } from "./types.ts";
import { boundedZstd } from "./zstd.ts";
async function collect(stream: ReadableStream<Uint8Array>, limit: number, abort?: () => void): Promise<Uint8Array> {
const reader = stream.getReader();
const chunks: Uint8Array[] = [];
let length = 0;
try {
for (;;) {
const { done, value } = await reader.read();
if (done) break;
length += value.length;
if (length > limit) throw new Error("Decoded data exceeds byte limit");
chunks.push(value);
}
} catch (error) {
abort?.();
await reader.cancel().catch(() => {});
throw error;
} finally {
reader.releaseLock();
}
const result = new Uint8Array(length);
let offset = 0;
for (const chunk of chunks) {
result.set(chunk, offset);
offset += chunk.length;
}
return result;
}
export async function decompress(data: Uint8Array, nativeDecoder?: string): Promise<Uint8Array> {
let format: "gzip" | "zstd" | "xz" | "bzip2" | undefined;
if (data[0] === 0x1f && data[1] === 0x8b) format = "gzip";
else if (data[0] === 0x28 && data[1] === 0xb5 && data[2] === 0x2f && data[3] === 0xfd) format = "zstd";
else if (
data[0] === 0xfd &&
data[1] === 0x37 &&
data[2] === 0x7a &&
data[3] === 0x58 &&
data[4] === 0x5a &&
data[5] === 0
)
format = "xz";
else if (data[0] === 0x42 && data[1] === 0x5a && data[2] === 0x68) format = "bzip2";
if (!format) return data;
if (format === "zstd") return boundedZstd(data);
if (format === "gzip") {
const stream = new Blob([new Uint8Array(data)]).stream().pipeThrough(new DecompressionStream("gzip"));
return collect(stream, LIMITS.unpacked);
}
if (!nativeDecoder)
throw new Error(`${format} needs --decoder with an explicitly configured native 7-Zip executable`);
// Native 7-Zip consumes the compressed member on stdin, returns bytes on stdout, writes no files.
const process = Bun.spawn(
[nativeDecoder, "e", "-so", "-si", `-t${format}`, "-bd", "-bsp0", "-bso0", "-bse2", "-mmt1"],
{
stdin: new Blob([new Uint8Array(data)]),
stdout: "pipe",
stderr: "pipe",
timeout: LIMITS.deadlineMs,
},
);
const results = await Promise.allSettled([
collect(process.stdout, LIMITS.unpacked, () => process.kill()),
collect(process.stderr, 65536, () => process.kill()),
process.exited,
]);
const output = results[0]!;
const status = results[2]!;
if (output.status === "rejected") throw output.reason;
if (status.status === "rejected" || status.value !== 0) throw new Error(`Native ${format} decoder failed`);
if (results[1]!.status === "rejected") throw new Error("Native decoder error output exceeds limit");
return output.value;
}
+343
View File
@@ -0,0 +1,343 @@
// SPDX-License-Identifier: MIT
import { mkdir, lstat, link, symlink, writeFile } from "node:fs/promises";
import { basename, dirname, isAbsolute, relative, resolve, sep } from "node:path";
import { abi } from "./abi.ts";
import { inspect, readBounded, type InputOptions } from "./metadata.ts";
import { bunVersion, nativeAssets, runtimeDependencies, type RuntimeDependencies } from "./native-assets.ts";
import { LIMITS, safePath, sha256, type Inspection, type PackageRecord, type PayloadEntry } from "./types.ts";
export { inspect } from "./metadata.ts";
export interface ConversionOptions extends InputOptions {
out: string;
package: string;
sourceBase?: string;
extractSources?: boolean;
windowsArtifact?: string;
installerUrl?: string;
wingetId?: string;
publisher?: string;
license?: string;
windowsVersion?: string;
runtimeDependencies?: Record<string, string>;
}
const yaml = (value: string) => JSON.stringify(value);
function selected(inspection: Inspection, name: string): PackageRecord {
const matches = inspection.packages.filter(pkg => pkg.name === name);
if (matches.length !== 1) throw new Error("Choose a package name with exactly one version in this input");
return matches[0]!;
}
function sourceUrls(pkg: PackageRecord, base?: string) {
return pkg.sources.map(source => {
if (source.url || !base) return source;
const directory = pkg.fields.directory?.[0];
const path = safePath(`${directory ? `${directory}/` : ""}${source.path}`);
const root = new URL(base.endsWith("/") ? base : `${base}/`);
if (!/^https?:$/.test(root.protocol) || root.username || root.password)
throw new Error("Source base must be public HTTP(S)");
return {
...source,
url: new URL(path.split("/").map(encodeURIComponent).join("/"), root).href,
};
});
}
function targetPath(root: string, path: string): string {
const target = resolve(root, safePath(path));
const rel = relative(root, target);
if (isAbsolute(rel) || rel === ".." || rel.startsWith(`..${sep}`)) throw new Error("Output path escapes root");
return target;
}
async function newFile(path: string, data: string | Uint8Array, mode?: number) {
await mkdir(dirname(path), { recursive: true });
await writeFile(path, data, { flag: "wx", mode });
}
async function extract(root: string, entries: PayloadEntry[]) {
const folded = new Set<string>();
const key = (value: string) => (process.platform === "win32" ? value.toLowerCase() : value);
const byPath = new Map(entries.map(entry => [key(entry.path), entry]));
for (const entry of entries) {
if (folded.has(key(entry.path))) throw new Error("Archive has colliding target paths");
folded.add(key(entry.path));
if (
process.platform === "win32" &&
entry.path
.split("/")
.some(part => /^(?:con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(part) || /[<>"|?*]|[ .]$/.test(part))
)
throw new Error("Archive path is not representable on Windows");
const parts = entry.path.split("/");
for (let count = 1; count < parts.length; count++) {
const ancestor = byPath.get(key(parts.slice(0, count).join("/")));
if (ancestor && ancestor.kind !== "directory") throw new Error("Archive entry traverses a file or link ancestor");
}
}
// Resolve the archive's complete link graph before creating any links or directories.
// Lexical normalization alone misses paths which traverse an alias followed by '..'.
const targets = new Map<string, string>();
for (const entry of entries.filter(entry => entry.kind === "symlink" || entry.kind === "hardlink")) {
if (!entry.link || isAbsolute(entry.link) || /[\\:\0]/.test(entry.link))
throw new Error("Payload link has an external target");
let parts =
entry.kind === "hardlink"
? entry.link.split("/")
: [...entry.path.split("/").slice(0, -1), ...entry.link.split("/")];
const resolved: string[] = [];
let offset = 0;
let expansions = 0;
while (offset < parts.length) {
const part = parts[offset++]!;
if (!part || part === ".") continue;
if (part === "..") {
if (!resolved.length) throw new Error("Payload link escapes output tree");
resolved.pop();
continue;
}
resolved.push(part);
const node = byPath.get(key(resolved.join("/")));
if (node?.kind === "symlink" || node?.kind === "hardlink") {
if (++expansions > 64) throw new Error("Payload link cycle or chain exceeds limit");
if (!node.link || isAbsolute(node.link) || /[\\:\0]/.test(node.link))
throw new Error("Payload link has an external target");
if (node.kind === "hardlink") resolved.length = 0;
else resolved.pop();
parts = [...node.link.split("/"), ...parts.slice(offset)];
offset = 0;
} else if (node?.kind === "file" && offset < parts.length)
throw new Error("Payload link traverses a regular file");
}
const destination = resolved.join("/");
if (entry.kind === "hardlink" && byPath.get(key(destination))?.kind !== "file")
throw new Error("Payload hardlink needs a regular archive file target");
targets.set(entry.path, destination);
}
await mkdir(root);
for (const entry of entries.filter(entry => entry.kind === "directory"))
await mkdir(targetPath(root, entry.path), { recursive: true });
for (const entry of entries.filter(entry => entry.kind === "file"))
await newFile(targetPath(root, entry.path), entry.data, entry.mode & 0o777);
// Links are created after all regular files; hardlink chains resolve to their regular file owner.
for (const entry of entries.filter(entry => entry.kind === "symlink" || entry.kind === "hardlink")) {
const destination = targetPath(root, entry.path);
await mkdir(dirname(destination), { recursive: true });
if (entry.kind === "hardlink") await link(targetPath(root, targets.get(entry.path)!), destination);
else await symlink(entry.link!, destination);
}
}
function winget(
options: ConversionOptions,
pkg: PackageRecord,
hash: string,
architecture: string,
dependencies: string[],
): string {
if (!options.wingetId || !/^[A-Za-z0-9][A-Za-z0-9+_-]*(?:\.[A-Za-z0-9][A-Za-z0-9+_-]*)+$/.test(options.wingetId))
throw new Error("WinGet needs a Publisher.Package identifier");
if (!options.publisher || !options.license || !options.installerUrl)
throw new Error("WinGet needs publisher, license and installer URL");
const url = new URL(options.installerUrl);
if (url.protocol !== "https:" || url.username || url.password) throw new Error("Installer URL must be public HTTPS");
if (!["x86", "x64", "arm", "arm64"].includes(architecture)) throw new Error("Unsupported Windows architecture");
const version = options.windowsVersion ?? pkg.version;
if (
!version ||
version.length > 128 ||
/[\\/:*?"<>|]/.test(version) ||
Array.from(version).some(character => character.charCodeAt(0) < 32)
)
throw new Error("WinGet version needs --windows-version when the source version uses unsupported characters");
const deps = dependencies.length
? `Dependencies:\n PackageDependencies:\n${dependencies.map(id => ` - PackageIdentifier: ${yaml(id)}\n`).join("")}`
: "";
return `# yaml-language-server: $schema=https://aka.ms/winget-manifest.singleton.1.12.0.schema.json\nPackageIdentifier: ${yaml(options.wingetId)}\nPackageVersion: ${yaml(version)}\nPackageLocale: en-US\nPublisher: ${yaml(options.publisher)}\nPackageName: ${yaml(pkg.name)}\nLicense: ${yaml(options.license)}\nShortDescription: ${yaml((pkg.description ?? pkg.name).split("\n")[0]!.slice(0, 256))}\n${deps}Installers:\n- Architecture: ${architecture}\n InstallerType: portable\n InstallerUrl: ${yaml(url.href)}\n InstallerSha256: ${hash.toUpperCase()}\n Commands:\n - ${yaml(pkg.name)}\nManifestType: singleton\nManifestVersion: 1.12.0\n`;
}
/** Preserve native PE payloads; foreign ABI inputs retain an explicit Windows rebuild contract. */
export async function convert(path: string, options: ConversionOptions): Promise<string> {
if (options.extractSources && options.format !== "source-archive")
throw new Error("Source extraction requires --format source-archive");
const { inspection, entries } = await inspect(path, options);
const pkg = selected(inspection, options.package);
const sources = sourceUrls(pkg, options.sourceBase);
const assets = options.windowsArtifact ? undefined : nativeAssets(inspection, pkg, options.runtimeDependencies);
let windows:
| {
bytes: Uint8Array;
file: string;
hash: string;
architecture: string;
imports: string[];
runtime: string;
dependencies: RuntimeDependencies;
}
| undefined;
if (options.windowsArtifact) {
const bytes = await readBounded(options.windowsArtifact, LIMITS.unpacked);
const format = abi(bytes);
if (
format.format !== "pe" ||
!format.executable ||
![2, 3].includes(format.subsystem ?? 0) ||
!format.architecture ||
!options.windowsArtifact.toLowerCase().endsWith(".exe")
)
throw new Error("Windows export needs an actual PE executable, never an ELF archive, DLL or renamed data");
const dependencies = runtimeDependencies(format.imports ?? [], options.runtimeDependencies);
if (options.wingetId && Object.keys(dependencies.bun).length)
throw new Error(
"WinGet needs WinGet package owners for external DLLs; Bun package mappings do not resolve its dependencies",
);
windows = {
bytes,
file: basename(options.windowsArtifact),
hash: sha256(bytes),
architecture: format.architecture,
imports: format.imports ?? [],
runtime: format.runtime ?? "windows",
dependencies,
};
}
const manifest =
windows && options.wingetId
? winget(options, pkg, windows.hash, windows.architecture, windows.dependencies.winget)
: undefined;
if (options.wingetId && !windows)
throw new Error("WinGet output requires --windows-artifact with a real rebuilt PE executable");
const root = resolve(options.out);
try {
await lstat(root);
throw new Error("Output root already exists; refusing replacement");
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
await mkdir(dirname(root), { recursive: true });
await mkdir(root);
await newFile(
targetPath(root, "package-contract.json"),
`${JSON.stringify({ ...inspection, selected: pkg.name, sources, signaturesVerified: false }, null, 2)}\n`,
);
if (windows) {
await newFile(targetPath(root, `bin/${windows.file}`), windows.bytes);
const npmName = pkg.name.toLowerCase().replace(/[^a-z0-9._-]/g, "-");
const packageJson = {
name: `@bun-os/${npmName}`,
version: bunVersion(options.windowsVersion ?? pkg.version),
private: true,
license: options.license ?? (pkg.licenses.length === 1 ? pkg.licenses[0] : "UNLICENSED"),
description: pkg.description,
os: ["win32"],
cpu: [({ x64: "x64", x86: "ia32", arm64: "arm64", arm: "arm" } as Record<string, string>)[windows.architecture]],
bin: { [pkg.name]: `bin/${windows.file}` },
dependencies: windows.dependencies.bun,
systemDependencies: Object.fromEntries(windows.dependencies.winget.map(id => [`winget:${id}`, "*"])),
bunOs: {
schema: 1,
upstreamVersion: pkg.version,
ecosystem: pkg.ecosystem,
artifactSha256: windows.hash,
architecture: windows.architecture,
runtime: windows.runtime,
imports: windows.imports,
importResolution: windows.dependencies.imports,
upstreamDependencies: pkg.dependencies,
sources,
sourceMetadataSha256: inspection.input.sha256,
rebuildProvenance: "owner-supplied PE artifact",
},
};
await newFile(targetPath(root, "package.json"), `${JSON.stringify(packageJson, null, 2)}\n`);
if (manifest) await newFile(targetPath(root, "winget.yaml"), manifest);
} else if (assets) {
await extract(targetPath(root, "payload"), entries);
await newFile(
targetPath(root, "native-assets.json"),
`${JSON.stringify({ ...assets, upstreamSources: sources }, null, 2)}\n`,
);
const npmName = pkg.name.toLowerCase().replace(/[^a-z0-9._-]/g, "-");
const packageJson = {
name: `@bun-os/${npmName}`,
version: bunVersion(options.windowsVersion ?? pkg.version),
private: true,
license: options.license ?? (pkg.licenses.length === 1 ? pkg.licenses[0] : "UNLICENSED"),
description: pkg.description,
os: ["win32"],
cpu: [({ x64: "x64", x86: "ia32", arm64: "arm64", arm: "arm" } as Record<string, string>)[assets.architecture]],
files: ["package-contract.json", "native-assets.json", "payload"],
exports: { "./native-assets": "./native-assets.json", "./payload/*": "./payload/*" },
dependencies: assets.dependencies,
systemDependencies: assets.systemDependencies,
bunOs: {
schema: 1,
packageKind: assets.packageKind,
upstreamVersion: pkg.version,
ecosystem: pkg.ecosystem,
architecture: assets.architecture,
runtime: assets.runtime,
sources,
dependencies: pkg.dependencies,
sourceMetadataSha256: inspection.input.sha256,
rebuildProvenance: "original native PE archive payload; no rebuild performed",
runtimeQualified: false,
},
};
await newFile(targetPath(root, "package.json"), `${JSON.stringify(packageJson, null, 2)}\n`);
} else {
const npmName = pkg.name.toLowerCase().replace(/[^a-z0-9._-]/g, "-");
const packageJson = {
name: `@bun-os/${npmName}`,
version: bunVersion(pkg.version),
private: true,
license: options.license ?? (pkg.licenses.length === 1 ? pkg.licenses[0] : "UNLICENSED"),
description: pkg.description,
files: ["package-contract.json", "port-job.json", ...(options.extractSources ? ["sources"] : [])],
bunOs: {
schema: 1,
packageKind: "source-contract",
upstreamVersion: pkg.version,
ecosystem: pkg.ecosystem,
sources,
dependencies: pkg.dependencies,
sourceMetadataSha256: inspection.input.sha256,
windowsArtifactProduced: false,
},
};
await newFile(targetPath(root, "package.json"), `${JSON.stringify(packageJson, null, 2)}\n`);
await newFile(
targetPath(root, "port-job.json"),
`${JSON.stringify(
{
schema: 1,
package: pkg.name,
sourceVersion: pkg.version,
sourceEcosystem: pkg.ecosystem,
target: "windows",
requiredAbi: "pe",
sources,
dependencies: pkg.dependencies,
sourceMetadataSha256: inspection.input.sha256,
inputs: inspection.payload,
tasks: [
"retrieve and verify every source artifact",
"map dependencies to the selected Windows runtime",
"port OS interfaces",
"build with the selected native toolchain",
"qualify the exact PE and runtime dependencies",
],
windowsArtifactProduced: false,
},
null,
2,
)}\n`,
);
}
if (options.extractSources) {
await extract(targetPath(root, "sources"), entries);
}
return root;
}
+95
View File
@@ -0,0 +1,95 @@
// SPDX-License-Identifier: MIT
import { decoder, LIMITS, type Fields, type PackageRecord, type SourceArtifact } from "./types.ts";
/** setup.ini retains current/previous/test versions and native dependency expressions. */
export function cygwin(data: Uint8Array): PackageRecord[] {
if (data.length > LIMITS.metadata) throw new Error("Cygwin metadata exceeds limit");
const packages: PackageRecord[] = [];
let name = "";
let fields: Fields = Object.create(null);
let common: Fields = Object.create(null);
let channel = "current";
let architecture: string | undefined;
let pending: { key: string; value: string } | undefined;
const emit = () => {
if (!name || !fields.version) return;
const all = { ...common, ...fields };
const artifacts: SourceArtifact[] = [];
for (const key of ["install", "source"]) {
if (!all[key]) continue;
const parts = all[key]![0]!.trim().split(/\s+/);
if (parts.length !== 3 || !/^\d+$/.test(parts[1]!) || !/^[a-fA-F0-9]+$/.test(parts[2]!))
throw new Error("Invalid Cygwin archive record");
const algorithm = ({ 32: "md5", 40: "sha1", 64: "sha256", 128: "sha512" } as Record<number, string>)[
parts[2]!.length
];
if (!algorithm || !Number.isSafeInteger(Number(parts[1]))) throw new Error("Invalid Cygwin archive integrity");
artifacts.push({
path: parts[0]!,
size: Number(parts[1]),
integrity: `${algorithm}:${parts[2]!.toLowerCase()}`,
});
}
packages.push({
ecosystem: "cygwin",
kind: all.install ? "binary" : "source",
name,
version: all.version![0]!,
architecture: all.arch?.[0] ?? architecture,
description: all.sdesc?.[0],
homepage: all.homepage?.[0],
licenses: all.license ?? [],
sources: artifacts,
dependencies: Object.fromEntries(
["requires", "depends2", "provides", "conflicts", "obsoletes", "build-depends"]
.filter(key => all[key])
.map(key => [key, all[key]!]),
),
fields: { ...all, channel: [channel] },
});
if (packages.length > LIMITS.entries) throw new Error("Too many Cygwin records");
};
for (const line of decoder.decode(data).split(/\r?\n/)) {
if (line.length > 65536) throw new Error("Cygwin line exceeds limit");
if (pending) {
pending.value += `\n${line}`;
if (pending.value.length > LIMITS.metadata) throw new Error("Cygwin quoted field exceeds limit");
if (line.endsWith('"')) {
fields[pending.key] = [pending.value.slice(1, -1)];
pending = undefined;
}
continue;
}
if (!line || line.startsWith("#")) continue;
if (line.startsWith("@ ")) {
emit();
name = line.slice(2).trim();
fields = Object.create(null);
common = Object.create(null);
channel = "current";
if (!/^[a-zA-Z0-9][a-zA-Z0-9_.+-]*$/.test(name)) throw new Error("Invalid Cygwin package name");
} else if (/^\[(prev|test|curr)\]$/.test(line)) {
emit();
common = { ...common, ...fields };
delete common.version;
delete common.install;
delete common.source;
fields = Object.create(null);
channel = line.slice(1, -1);
} else {
const match = /^([a-z0-9_-]+):\s*(.*)$/.exec(line);
if (!match) throw new Error("Invalid Cygwin setup line");
if (!name) {
if (match[1] === "arch") architecture = match[2]!;
continue;
}
if (fields[match[1]!]) throw new Error("Duplicate Cygwin field");
const value = match[2]!;
if (value.startsWith('"') && !value.endsWith('"')) pending = { key: match[1]!, value };
else fields[match[1]!] = [value.startsWith('"') ? value.slice(1, -1) : value];
}
}
if (pending) throw new Error("Unterminated Cygwin quoted field");
emit();
return packages;
}
+139
View File
@@ -0,0 +1,139 @@
// SPDX-License-Identifier: MIT
import { decoder, LIMITS, type Ecosystem, type Fields, type PackageRecord, type SourceArtifact } from "./types.ts";
/** Debian control fields: case-insensitive names, folded values and blank-line stanzas. */
export function deb822(data: Uint8Array): Fields[] {
if (data.length > LIMITS.metadata) throw new Error("Debian metadata exceeds limit");
let text = decoder.decode(data);
// .dsc files may carry a cleartext OpenPGP signature. Preserve its presence, never its trust claim.
const signed = text.startsWith("-----BEGIN PGP SIGNED MESSAGE-----");
if (signed) {
const lines = text.split(/\r?\n/);
const start = lines.indexOf("");
const end = lines.indexOf("-----BEGIN PGP SIGNATURE-----", start + 1);
if (start < 1 || end < start || !lines.includes("-----END PGP SIGNATURE-----"))
throw new Error("Malformed Debian cleartext signature");
text = lines
.slice(start + 1, end)
.map(line => (line.startsWith("- ") ? line.slice(2) : line))
.join("\n");
}
const rows: Fields[] = [];
let row: Fields = Object.create(null);
let previous = "";
for (const line of [...text.split(/\r?\n/), ""]) {
if (line.length > 65536) throw new Error("Debian field line exceeds limit");
if (!line.length) {
if (Object.keys(row).length) rows.push(row);
if (rows.length > LIMITS.entries) throw new Error("Too many Debian stanzas");
row = Object.create(null);
previous = "";
} else if (/^[ \t]/.test(line)) {
if (!previous) throw new Error("Orphan Debian continuation");
const text = `${row[previous]![0]}\n${line.slice(1)}`;
if (text.length > 1024 * 1024) throw new Error("Debian field exceeds limit");
row[previous] = [text];
} else if (!line.startsWith("#")) {
const at = line.indexOf(":");
const name = line.slice(0, at).toLowerCase();
if (at < 1 || !/^[a-z0-9][a-z0-9-]*$/.test(name) || row[name])
throw new Error("Invalid or duplicate Debian field");
row[name] = [line.slice(at + 1).replace(/^[ \t]+/, "")];
previous = name;
}
}
if (signed) for (const fields of rows) fields["openpgp-clearsigned"] = ["present-unverified"];
return rows;
}
export function required(fields: Fields, key: string): string {
const value = fields[key]?.[0];
if (!value) throw new Error(`Missing package field: ${key}`);
return value;
}
function checksumFiles(fields: Fields): SourceArtifact[] {
const byPath = new Map<string, SourceArtifact>();
for (const [key, algorithm, digits] of [
["files", "md5", 32],
["checksums-sha1", "sha1", 40],
["checksums-sha256", "sha256", 64],
] as const) {
for (const line of (fields[key]?.[0] ?? "").split("\n").filter(line => line.trim())) {
const parts = line.trim().split(/\s+/);
// Sources/.dsc use three columns; .changes may add section and priority columns.
const [hash, size] = parts;
const path = parts.at(-1);
if (
!hash ||
!size ||
!path ||
![3, 5].includes(parts.length) ||
!new RegExp(`^[a-fA-F0-9]{${digits}}$`).test(hash) ||
!/^\d+$/.test(size)
)
throw new Error("Invalid Debian source checksum");
const count = Number(size);
if (!Number.isSafeInteger(count)) throw new Error("Debian source size exceeds safe integer");
const old = byPath.get(path);
if (old && old.size !== count) throw new Error("Conflicting source artifact sizes");
byPath.set(path, { path, size: count, integrity: `${algorithm}:${hash.toLowerCase()}` });
}
}
return [...byPath.values()];
}
export function debianRecords(rows: Fields[], ecosystem: Ecosystem): PackageRecord[] {
return rows.map(fields => {
const name = fields.package?.[0] ?? required(fields, "source");
if (!/^[a-z0-9][a-z0-9+.-]+$/.test(name)) throw new Error("Invalid Debian package name");
const version = required(fields, "version");
const source = !!fields.directory || !!fields["checksums-sha256"] || !!fields["binary"];
const sources = checksumFiles(fields);
if (fields.filename) {
const artifact: SourceArtifact = { path: required(fields, "filename") };
if (fields.size) {
const size = required(fields, "size");
if (!/^\d+$/.test(size) || !Number.isSafeInteger(Number(size))) throw new Error("Invalid Debian package size");
artifact.size = Number(size);
}
if (fields.sha256) {
const hash = required(fields, "sha256");
if (!/^[a-fA-F0-9]{64}$/.test(hash)) throw new Error("Invalid Debian package SHA256");
artifact.integrity = `sha256:${hash.toLowerCase()}`;
}
sources.push(artifact);
}
const dependencies: Record<string, string[]> = Object.create(null);
for (const key of [
"pre-depends",
"depends",
"recommends",
"suggests",
"enhances",
"provides",
"conflicts",
"breaks",
"replaces",
"build-depends",
"build-depends-indep",
"build-depends-arch",
"build-conflicts",
]) {
if (fields[key]) dependencies[key] = fields[key]!;
}
return {
ecosystem,
kind: source ? "source" : "binary",
name,
version,
architecture: fields.architecture?.[0],
description: fields.description?.[0],
homepage: fields.homepage?.[0],
licenses: fields.license ?? [],
dependencies,
sources,
fields,
};
});
}
+166
View File
@@ -0,0 +1,166 @@
// SPDX-License-Identifier: MIT
import { basename, resolve } from "node:path";
import { ar } from "./ar.ts";
import { abi } from "./abi.ts";
import { deb822, debianRecords } from "./deb822.ts";
import { pkginfo, srcinfo } from "./alpm.ts";
import { cygwin } from "./cygwin.ts";
import { decompress } from "./compression.ts";
import { tar } from "./tar.ts";
import {
decoder,
LIMITS,
sha256,
type Ecosystem,
type InputFormat,
type Inspection,
type PackageRecord,
type PayloadEntry,
} from "./types.ts";
export interface InputOptions {
format: InputFormat;
ecosystem: Ecosystem;
decoder?: string;
metadataFile?: string;
metadataFormat?: InputFormat;
}
export async function readBounded(path: string, limit = LIMITS.packed): Promise<Uint8Array> {
const file = Bun.file(path);
if (!(await file.exists()) || file.size > limit) throw new Error(`Missing or oversized input: ${path}`);
const bytes = new Uint8Array(await file.arrayBuffer());
if (bytes.length > limit) throw new Error("Input grew beyond byte limit");
return bytes;
}
function sourceMetadata(entries: PayloadEntry[], ecosystem: Ecosystem): PackageRecord[] {
const files = entries.filter(entry => entry.kind === "file");
const srcinfos = files.filter(entry => basename(entry.path) === ".SRCINFO");
if (srcinfos.length) return srcinfos.flatMap(entry => srcinfo(entry.data, ecosystem));
const pkginfos = files.filter(entry => basename(entry.path) === ".PKGINFO");
if (pkginfos.length) return pkginfos.flatMap(entry => pkginfo(entry.data, ecosystem));
const descriptors = files.filter(entry => entry.path.endsWith(".dsc"));
if (descriptors.length) return descriptors.flatMap(entry => debianRecords(deb822(entry.data), ecosystem));
const controls = files.filter(entry => /(^|\/)debian\/control$/.test(entry.path));
return controls.flatMap(entry => {
const prefix = entry.path.slice(0, -"debian/control".length);
const changelog = files.find(file => file.path === `${prefix}debian/changelog`);
if (!changelog) throw new Error("Debian source archive needs its changelog to preserve the source version");
const first = decoder.decode(changelog.data).split(/\r?\n/, 1)[0]!;
const version = /^[^\s]+\s+\(([^)]+)\)/.exec(first)?.[1];
if (!version) throw new Error("Invalid Debian source changelog header");
const rows = deb822(entry.data);
return debianRecords(
rows.map((row, index) => ({
...row,
version: [version],
...(index === 0 ? { binary: rows.slice(1).map(binary => binary.package?.[0] ?? "") } : {}),
})),
ecosystem,
);
});
}
export async function inspect(
path: string,
options: InputOptions,
): Promise<{ inspection: Inspection; entries: PayloadEntry[] }> {
const formats =
options.ecosystem === "cygwin"
? ["cygwin", "source-archive"]
: ["alpm", "msys2", "mingw"].includes(options.ecosystem)
? ["pkginfo", "srcinfo", "alpm-archive", "source-archive"]
: ["deb", "deb822", "source-archive"];
if (!formats.includes(options.format)) throw new Error("Input format and package ecosystem do not match");
const data = await readBounded(path);
let packages: PackageRecord[];
let entries: PayloadEntry[] = [];
let companion: Inspection["companion"];
if (options.format === "deb") {
const members = ar(data);
if (members[0]?.name !== "debian-binary" || !/^2\.\d+\n/.test(decoder.decode(members[0].data)))
throw new Error("Unsupported Debian binary format");
const controls = members.filter(member => /^control\.tar(?:\.(?:gz|xz|zst|bz2))?$/.test(member.name));
const payloads = members.filter(member => /^data\.tar(?:\.(?:gz|xz|zst|bz2))?$/.test(member.name));
if (
controls.length !== 1 ||
payloads.length !== 1 ||
members.indexOf(controls[0]!) >= members.indexOf(payloads[0]!)
)
throw new Error("Debian archive needs ordered control/data members");
const controlIndex = members.indexOf(controls[0]!);
const dataIndex = members.indexOf(payloads[0]!);
for (let index = 1; index < dataIndex; index++) {
if (index !== controlIndex && !members[index]!.name.startsWith("_"))
throw new Error("Unknown Debian member before its data archive");
}
const control = tar(await decompress(controls[0]!.data, options.decoder)).find(
entry => entry.path === "control" && entry.kind === "file",
);
if (!control) throw new Error("Debian archive has no control file");
packages = debianRecords(deb822(control.data), options.ecosystem);
if (packages.length !== 1) throw new Error("Debian binary archive must have one control stanza");
entries = tar(await decompress(payloads[0]!.data, options.decoder));
} else if (options.format === "deb822") packages = debianRecords(deb822(data), options.ecosystem);
else if (options.format === "pkginfo") packages = pkginfo(data, options.ecosystem);
else if (options.format === "srcinfo") packages = srcinfo(data, options.ecosystem);
else if (options.format === "cygwin") packages = cygwin(data);
else {
entries = tar(await decompress(data, options.decoder));
if (options.format === "alpm-archive") {
const metadata = entries.find(entry => entry.path === ".PKGINFO" && entry.kind === "file");
if (!metadata) throw new Error("ALPM archive has no root .PKGINFO");
packages = pkginfo(metadata.data, options.ecosystem);
} else if (options.metadataFile) {
if (!options.metadataFormat || !["deb822", "pkginfo", "srcinfo", "cygwin"].includes(options.metadataFormat))
throw new Error("Source companion needs a metadata format");
const source = await inspect(options.metadataFile, {
format: options.metadataFormat,
ecosystem: options.ecosystem,
});
packages = source.inspection.packages;
companion = source.inspection.input;
for (const pkg of packages)
for (const artifact of pkg.sources) {
if (basename(artifact.path) !== basename(path)) continue;
if (artifact.size !== undefined && artifact.size !== data.length)
throw new Error("Source archive differs from companion size");
if (artifact.integrity) {
const [algorithm, expected] = artifact.integrity.split(":");
if (!["md5", "sha1", "sha256", "sha512"].includes(algorithm!))
throw new Error("Unsupported source checksum algorithm");
const actual = new Bun.CryptoHasher(algorithm as "md5" | "sha1" | "sha256" | "sha512")
.update(data)
.digest("hex");
if (actual !== expected) throw new Error("Source archive checksum differs from companion metadata");
}
}
} else packages = sourceMetadata(entries, options.ecosystem);
if (!packages.length) throw new Error("Archive has no supported package source metadata");
}
const payload = entries.map(entry => ({
path: entry.path,
kind: entry.kind,
mode: entry.mode,
link: entry.link,
size: entry.data.length,
sha256: entry.kind === "file" ? sha256(entry.data) : undefined,
abi: entry.kind === "file" ? abi(entry.data) : undefined,
}));
return {
inspection: {
schema: 1,
input: {
path: resolve(path),
sha256: sha256(data),
size: data.length,
format: options.format,
},
companion,
packages,
payload,
},
entries,
};
}
@@ -0,0 +1,174 @@
// SPDX-License-Identifier: MIT
import { basename } from "node:path";
import { type Inspection, type PackageRecord } from "./types.ts";
const NUMBER = "(?:0|[1-9][0-9]*)";
const PRERELEASE = "(?:0|[1-9][0-9]*|[0-9A-Za-z-]*[A-Za-z-][0-9A-Za-z-]*)";
const VERSION = new RegExp(
`^${NUMBER}\\.${NUMBER}\\.${NUMBER}(?:-${PRERELEASE}(?:\\.${PRERELEASE})*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$`,
);
export const bunVersion = (value: string) => (VERSION.test(value) ? value : "0.0.0");
const WINGET_ID = /^[A-Za-z0-9][A-Za-z0-9+_-]*(?:\.[A-Za-z0-9][A-Za-z0-9+_-]*)+$/;
const BUN_OWNER = /^bun:((?:@[a-z0-9][a-z0-9._-]*\/)?[a-z0-9][a-z0-9._-]*)@(.+)$/;
export type RuntimeOwner = { kind: "winget"; id: string } | { kind: "bun"; name: string; version: string };
export type ImportResolution =
| { dll: string; kind: "system" }
| { dll: string; kind: "bundled"; paths: string[] }
| { dll: string; kind: "external"; owner: RuntimeOwner };
interface BundledLibrary {
path: string;
sha256: string;
}
export interface RuntimeDependencies {
imports: ImportResolution[];
bun: Record<string, string>;
winget: string[];
}
function runtimeOwner(value: string): RuntimeOwner {
const bun = BUN_OWNER.exec(value);
if (bun) {
if (!VERSION.test(bun[2]!)) throw new Error("Bun runtime dependency needs an exact semantic version");
return { kind: "bun", name: bun[1]!, version: bun[2]! };
}
const id = value.startsWith("winget:") ? value.slice(7) : value;
if (!WINGET_ID.test(id)) throw new Error("Runtime owner must be Publisher.Package or bun:package@VERSION");
return { kind: "winget", id };
}
export function runtimeDependencies(
imports: string[],
mappings: Record<string, string> = {},
bundled: Map<string, BundledLibrary[]> = new Map(),
): RuntimeDependencies {
const owners = new Map<string, RuntimeOwner>();
for (const [dll, owner] of Object.entries(mappings)) {
const key = dll.toLowerCase();
if (!/^[a-z0-9_.+-]+\.dll$/.test(key) || owners.has(key))
throw new Error("Invalid or duplicate DLL runtime mapping");
owners.set(key, runtimeOwner(owner));
}
const bun: Record<string, string> = Object.create(null);
const winget = new Set<string>();
const resolutions: ImportResolution[] = [];
for (const dll of new Set(imports)) {
if (/^(?:api-ms-|ext-ms-)/.test(dll) || WINDOWS_DLLS.has(dll)) {
resolutions.push({ dll, kind: "system" });
continue;
}
const files = bundled.get(dll);
if (files?.length) {
if (new Set(files.map(file => file.sha256)).size !== 1) throw new Error(`Ambiguous bundled DLL ${dll}`);
resolutions.push({ dll, kind: "bundled", paths: files.map(file => file.path) });
continue;
}
const owner = owners.get(dll);
if (!owner) throw new Error(`Native PE imports ${dll}; configure its runtime package owner explicitly`);
resolutions.push({ dll, kind: "external", owner });
if (owner.kind === "winget") winget.add(owner.id);
else {
if (bun[owner.name] && bun[owner.name] !== owner.version)
throw new Error(`Conflicting Bun runtime versions for ${owner.name}`);
bun[owner.name] = owner.version;
}
}
return { imports: resolutions, bun, winget: [...winget] };
}
export function nativeAssets(inspection: Inspection, pkg: PackageRecord, mappings?: Record<string, string>) {
if (pkg.kind !== "binary" || inspection.payload.some(file => file.abi?.format === "elf")) return undefined;
const images = inspection.payload.filter(file => file.kind === "file" && file.abi?.format === "pe");
if (!images.length) return undefined;
const architectures = new Set(images.map(file => file.abi!.architecture));
const architecture = images[0]!.abi!.architecture;
if (architectures.size !== 1 || !architecture || !["x86", "x64", "arm", "arm64"].includes(architecture))
throw new Error("Native payload needs one supported, consistent PE architecture");
const bundled = new Map<string, BundledLibrary[]>();
for (const image of images) {
if (!image.abi!.library && !image.abi!.executable)
throw new Error("Payload PE is not an executable image or a DLL");
if (image.abi!.library) {
const name = basename(image.path).toLowerCase();
const files = bundled.get(name) ?? [];
files.push({ path: `payload/${image.path}`, sha256: image.sha256! });
bundled.set(name, files);
}
}
const files = images.map(image => ({
path: `payload/${image.path}`,
sha256: image.sha256!,
abi: image.abi!,
...runtimeDependencies(image.abi!.imports ?? [], mappings, bundled),
}));
const dependencies: Record<string, string> = Object.create(null);
for (const file of files)
for (const [name, version] of Object.entries(file.bun)) {
if (dependencies[name] && dependencies[name] !== version)
throw new Error(`Conflicting Bun runtime versions for ${name}`);
dependencies[name] = version;
}
return {
schema: 1,
packageKind: images.every(image => image.abi!.library) ? "native-library" : "native-assets",
architecture,
runtime: [...new Set(images.map(image => image.abi!.runtime))],
libraries: files.filter(file => file.abi.library),
images: files,
payload: inspection.payload.map(file => ({ ...file, path: `payload/${file.path}` })),
dependencies,
systemDependencies: Object.fromEntries(
[...new Set(files.flatMap(file => file.winget))].map(id => [`winget:${id}`, "*"]),
),
upstreamDependencies: pkg.dependencies,
inputArchiveSha256: inspection.input.sha256,
upstreamSources: pkg.sources,
upstreamLicenses: pkg.licenses,
payloadBytesPreserved: true,
signaturesVerified: false,
runtimeQualified: false,
};
}
// Windows OS owners only. VC redistributables, Cygwin and MSYS2 remain external dependencies.
const WINDOWS_DLLS = new Set([
"kernel32.dll",
"kernelbase.dll",
"ntdll.dll",
"user32.dll",
"gdi32.dll",
"gdi32full.dll",
"advapi32.dll",
"shell32.dll",
"ole32.dll",
"oleaut32.dll",
"combase.dll",
"comdlg32.dll",
"ws2_32.dll",
"bcrypt.dll",
"bcryptprimitives.dll",
"crypt32.dll",
"secur32.dll",
"shlwapi.dll",
"version.dll",
"winmm.dll",
"imm32.dll",
"msvcrt.dll",
"ucrtbase.dll",
"setupapi.dll",
"iphlpapi.dll",
"dnsapi.dll",
"dwmapi.dll",
"d3d11.dll",
"d3d12.dll",
"dxgi.dll",
"winhttp.dll",
"normaliz.dll",
"mpr.dll",
"cfgmgr32.dll",
"wsock32.dll",
"dbghelp.dll",
"userenv.dll",
]);
+116
View File
@@ -0,0 +1,116 @@
// SPDX-License-Identifier: MIT
import { decoder, LIMITS, safePath, type PayloadEntry } from "./types.ts";
const string = (data: Uint8Array) =>
decoder.decode(data.subarray(0, data.indexOf(0) < 0 ? data.length : data.indexOf(0)));
function number(data: Uint8Array): number {
if (data[0]! & 0x80) {
let value = BigInt(data[0]! & 0x7f);
for (const byte of data.subarray(1)) value = value * 256n + BigInt(byte);
if (value > BigInt(Number.MAX_SAFE_INTEGER)) throw new Error("Tar integer exceeds limit");
return Number(value);
}
const value = string(data).trim();
if (value && !/^[0-7]+$/.test(value)) throw new Error("Invalid tar octal number");
return value ? Number.parseInt(value, 8) : 0;
}
function pax(data: Uint8Array): Record<string, string> {
const fields: Record<string, string> = Object.create(null);
for (let offset = 0; offset < data.length;) {
let space = offset;
while (space < data.length && data[space] !== 32) space++;
const text = decoder.decode(data.subarray(offset, space));
if (!/^\d+$/.test(text)) throw new Error("Invalid PAX length");
const length = Number(text);
if (
!Number.isSafeInteger(length) ||
length < space - offset + 3 ||
length > data.length - offset ||
data[offset + length - 1] !== 10
)
throw new Error("Truncated PAX record");
const record = decoder.decode(data.subarray(space + 1, offset + length - 1));
const equal = record.indexOf("=");
if (equal < 1) throw new Error("Invalid PAX field");
fields[record.slice(0, equal)] = record.slice(equal + 1);
offset += length;
}
return fields;
}
/** ustar, GNU long names and PAX metadata, without filesystem extraction. */
export function tar(data: Uint8Array): PayloadEntry[] {
if (data.length > LIMITS.unpacked) throw new Error("Tar exceeds unpacked byte limit");
const entries: PayloadEntry[] = [];
const names = new Set<string>();
let local: Record<string, string> = Object.create(null);
let global: Record<string, string> = Object.create(null);
let terminated = false;
for (let offset = 0; offset + 512 <= data.length;) {
const header = data.subarray(offset, offset + 512);
if (header.every(byte => byte === 0)) {
if (offset + 1024 > data.length || !data.subarray(offset).every(byte => byte === 0))
throw new Error("Incomplete tar end marker or trailing payload");
terminated = true;
break;
}
let checksum = 0;
for (let i = 0; i < 512; i++) checksum += i >= 148 && i < 156 ? 32 : header[i]!;
if (number(header.subarray(148, 156)) !== checksum) throw new Error("Tar header checksum mismatch");
const attributes = { ...global, ...local };
if (Object.keys(attributes).some(key => key.startsWith("GNU.sparse.") || key === "SCHILY.realsize"))
throw new Error("Sparse tar payload needs a sparse-aware archive owner");
const size = attributes.size === undefined ? number(header.subarray(124, 136)) : Number(attributes.size);
const start = offset + 512;
if (!Number.isSafeInteger(size) || size < 0 || size > data.length - start) throw new Error("Truncated tar payload");
const payload = data.subarray(start, start + size);
const flag = header[156] ?? 0;
offset = start + Math.ceil(size / 512) * 512;
if (flag === 120 || flag === 103) {
if (size > LIMITS.metadata) throw new Error("Tar PAX metadata exceeds limit");
if (flag === 103) global = { ...global, ...pax(payload) };
else local = { ...local, ...pax(payload) };
continue;
}
if (flag === 76 || flag === 75) {
if (size > 65536) throw new Error("GNU tar name exceeds limit");
local[flag === 76 ? "path" : "linkpath"] = string(payload).replace(/\n$/, "");
continue;
}
// Old GNU headers use this region for timestamps and sparse metadata, not the ustar prefix.
const prefix = decoder.decode(header.subarray(257, 263)) === "ustar\0" ? string(header.subarray(345, 500)) : "";
const raw = attributes.path ?? `${prefix ? `${prefix}/` : ""}${string(header.subarray(0, 100))}`;
local = Object.create(null);
if (raw === "." || raw === "./") continue;
const path = safePath(raw);
if (names.has(path)) throw new Error(`Duplicate tar path: ${path}`);
names.add(path);
const kind =
flag === 0 || flag === 48
? "file"
: flag === 53
? "directory"
: flag === 50
? "symlink"
: flag === 49
? "hardlink"
: undefined;
if (!kind) throw new Error(`Unsupported tar entry type: ${flag}`);
const link =
kind === "symlink" || kind === "hardlink"
? (attributes.linkpath ?? string(header.subarray(157, 257)))
: undefined;
if (link?.includes("\0")) throw new Error("Invalid tar link");
entries.push({
path,
kind,
mode: number(header.subarray(100, 108)),
link,
data: kind === "file" ? payload : new Uint8Array(),
});
if (entries.length > LIMITS.entries) throw new Error("Tar entry count exceeds limit");
}
if (!terminated) throw new Error("Tar has no complete end marker");
return entries;
}
+79
View File
@@ -0,0 +1,79 @@
// SPDX-License-Identifier: MIT
export type Ecosystem = "debian" | "ubuntu" | "cosmic" | "alpm" | "msys2" | "mingw" | "cygwin";
export type InputFormat = "deb" | "deb822" | "pkginfo" | "srcinfo" | "cygwin" | "alpm-archive" | "source-archive";
export type Fields = Record<string, string[]>;
export interface SourceArtifact {
path: string;
size?: number;
integrity?: string;
url?: string;
}
export interface PackageRecord {
ecosystem: Ecosystem;
kind: "binary" | "source";
name: string;
version: string;
architecture?: string;
description?: string;
homepage?: string;
licenses: string[];
dependencies: Record<string, string[]>;
sources: SourceArtifact[];
fields: Fields;
}
export interface PayloadEntry {
path: string;
kind: "file" | "directory" | "symlink" | "hardlink";
mode: number;
link?: string;
data: Uint8Array;
}
export interface Abi {
format: "elf" | "pe" | "script" | "data";
architecture?: string;
bits?: number;
runtime?: "windows" | "cygwin" | "msys2" | "linux";
imports?: string[];
executable?: boolean;
library?: boolean;
subsystem?: number;
elfOsAbi?: number;
}
export interface Inspection {
schema: 1;
input: { path: string; sha256: string; size: number; format: InputFormat };
companion?: { path: string; sha256: string; size: number; format: InputFormat };
packages: PackageRecord[];
payload: Array<Omit<PayloadEntry, "data"> & { size: number; sha256?: string; abi?: Abi }>;
}
export interface Limits {
packed: number;
unpacked: number;
entries: number;
metadata: number;
deadlineMs: number;
}
export const LIMITS: Limits = {
packed: 128 * 1024 * 1024,
unpacked: 256 * 1024 * 1024,
entries: 100_000,
metadata: 16 * 1024 * 1024,
deadlineMs: 30_000,
};
export const decoder = new TextDecoder("utf-8", { fatal: true });
export const sha256 = (data: Uint8Array) => new Bun.CryptoHasher("sha256").update(data).digest("hex");
export function safePath(value: string): string {
if (value.startsWith("/") || /[\\:\0\r\n]/.test(value)) throw new Error(`Unsafe archive path: ${value}`);
const parts = value.split("/").filter(part => part !== "." && part !== "");
if (!parts.length || parts.some(part => part === "..")) throw new Error(`Unsafe archive path: ${value}`);
return parts.join("/");
}
+76
View File
@@ -0,0 +1,76 @@
// SPDX-License-Identifier: MIT
import { LIMITS } from "./types.ts";
// Frame layout: https://github.com/facebook/zstd/blob/dev/doc/zstd_compression_format.md
// Bun's decoder streams frames with unknown size and known frames over 16 MiB; bound the maximum output
// of every block before invoking it. A forged content-size field alone is insufficient.
export function boundedZstd(data: Uint8Array): Uint8Array {
if (
data.length > LIMITS.packed ||
data.length < 5 ||
data[0] !== 0x28 ||
data[1] !== 0xb5 ||
data[2] !== 0x2f ||
data[3] !== 0xfd
)
throw new Error("Expected one bounded Zstandard frame");
let offset = 4;
const take = (size: number): Uint8Array => {
if (size > data.length - offset) throw new Error("Truncated Zstandard frame");
const bytes = data.subarray(offset, offset + size);
offset += size;
return bytes;
};
const integer = (size: number): bigint => {
const bytes = take(size);
let value = 0n;
for (let index = bytes.length - 1; index >= 0; index--) value = value * 256n + BigInt(bytes[index]!);
return value;
};
const descriptor = take(1)[0]!;
if (descriptor & 8) throw new Error("Reserved Zstandard frame descriptor bit");
const single = !!(descriptor & 32);
const contentFlag = descriptor >> 6;
let window = 0;
if (!single) {
const descriptor = take(1)[0]!;
const base = 2 ** (10 + (descriptor >> 3));
window = base + (base / 8) * (descriptor & 7);
if (window > LIMITS.unpacked) throw new Error("Zstandard window exceeds byte limit");
}
const dictionarySize = [0, 1, 2, 4][descriptor & 3]!;
if (integer(dictionarySize) !== 0n) throw new Error("Zstandard dictionary is not configured");
const contentSizeBytes = contentFlag === 0 ? (single ? 1 : 0) : [0, 2, 4, 8][contentFlag]!;
const contentSize = contentSizeBytes ? integer(contentSizeBytes) + (contentSizeBytes === 2 ? 256n : 0n) : undefined;
if (contentSize !== undefined && contentSize > BigInt(LIMITS.unpacked))
throw new Error("Zstandard decoded size exceeds byte limit");
if (single) window = Number(contentSize);
const blockMaximum = Math.min(window, 128 * 1024);
let worstOutput = 0;
let knownOutput = 0;
let blocks = 0;
for (;;) {
if (++blocks > LIMITS.entries) throw new Error("Zstandard block count exceeds limit");
const header = take(3);
const value = header[0]! + (header[1]! << 8) + (header[2]! << 16);
const last = !!(value & 1);
const type = (value >> 1) & 3;
const size = value >>> 3;
if (type === 3 || size > blockMaximum) throw new Error("Invalid Zstandard block size or type");
worstOutput += type === 2 ? blockMaximum : size;
if (type !== 2) knownOutput += size;
if (worstOutput > LIMITS.unpacked || (contentSize !== undefined && BigInt(knownOutput) > contentSize))
throw new Error("Zstandard blocks exceed decoded byte bound");
take(type === 1 ? 1 : size);
if (last) break;
}
if (descriptor & 4) take(4);
if (offset !== data.length)
throw new Error("Zstandard trailing data, skippable frames or multiple frames are not accepted");
if (contentSize !== undefined && BigInt(worstOutput) < contentSize)
throw new Error("Zstandard declared size exceeds its block output bound");
const decoded = Bun.zstdDecompressSync(data);
if (decoded.length > worstOutput || (contentSize !== undefined && decoded.length !== Number(contentSize)))
throw new Error("Zstandard decoded size differs from its frame bounds");
return decoded;
}
+8
View File
@@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"moduleDetection": "force",
"verbatimModuleSyntax": true
},
"include": ["src/**/*.ts"]
}
+1118
View File
File diff suppressed because it is too large. Load diff
+52
View File
@@ -0,0 +1,52 @@
#!/usr/bin/env bun
// SPDX-License-Identifier: MIT
import { runPackageCLI } from "../packages/package-converter/src/cli.ts";
import sourceLock from "../sources.lock.json";
import manifest from "../package.json";
const apt = sourceLock.repositories.find(source => source.id === "apt");
if (!apt) throw new Error("The distribution has no pinned APT source owner");
const usage = `Bun OS APT package bridge
apt.exe inspect INPUT --format FORMAT --ecosystem ECOSYSTEM
apt.exe convert INPUT --format FORMAT --ecosystem ECOSYSTEM --package NAME --out NEW_DIRECTORY
apt.exe upstream
apt.exe --version
Use inspect --help for source, Windows artifact and runtime dependency options.
Conversion emits Bun package metadata and WinGet manifests for rebuilt PE executables.
The native libapt-pkg installation engine is a separate port qualification.`;
const [action, ...args] = Bun.argv.slice(2);
try {
if (!action || action === "--help" || action === "help") console.log(usage);
else if (action === "--version" || action === "version")
console.log(
JSON.stringify({
product: "bun-os-apt",
version: manifest.version,
bunVersion: Bun.version,
bunRevision: Bun.revision,
platform: process.platform,
architecture: process.arch,
capabilities: ["package-inspection", "source-contract", "bun-package", "winget-manifest"],
nativeLibaptEngine: false,
}),
);
else if (action === "upstream")
console.log(
JSON.stringify(
{
...apt,
officialRepository: "https://salsa.debian.org/apt-team/apt",
},
null,
2,
),
);
else if (action === "inspect" || action === "convert") await runPackageCLI([action, ...args]);
else throw new Error(`Unknown APT bridge command ${action}; use --help`);
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
+65
View File
@@ -0,0 +1,65 @@
#!/usr/bin/env bun
// SPDX-License-Identifier: MIT
import { join, resolve } from "node:path";
import { parseArgs } from "node:util";
import { buildApp } from "../infra/app-build.ts";
import { command, identity, packageRoot, readLock } from "./distribution.ts";
async function main() {
const { values } = parseArgs({
args: Bun.argv.slice(2),
options: {
out: { type: "string" },
config: { type: "string", default: join(packageRoot, "infra/apps.json") },
help: { type: "boolean", short: "h" },
},
allowPositionals: false,
});
if (values.help) {
console.log(
"bun src/build-apt.ts --out NEW_DIRECTORY [--config FILE]\nCompile apt.exe through the single app owner and qualify its actual version contract.",
);
return;
}
if (!values.out) throw new Error("An explicit fresh --out directory is required");
if (process.platform !== "win32" || process.arch !== "x64")
throw new Error("Qualify apt.exe with the selected native Windows x64 Bun owner");
const output = resolve(values.out);
const receipt = await buildApp(resolve(values.config!), "apt", "windows-x64", output);
const executable = receipt.artifact.path;
const before = await identity(executable);
const version = JSON.parse(await command([executable, "--version"], output, 15_000));
if (
version.product !== "bun-os-apt" ||
version.bunRevision !== receipt.runtime.revision ||
version.bunVersion !== receipt.runtime.version ||
version.platform !== "win32" ||
version.architecture !== "x64" ||
version.nativeLibaptEngine !== false
)
throw new Error("The executing APT artifact differs from its selected runtime or capability contract");
const after = await identity(executable);
if (
before.sha256 !== receipt.artifact.sha256 ||
before.bytes !== receipt.artifact.bytes ||
JSON.stringify(before) !== JSON.stringify(after)
)
throw new Error("APT artifact changed during native execution qualification");
receipt.aptSource = (await readLock()).repositories.find(source => source.id === "apt");
receipt.qualification.nativeExecution = true;
receipt.appQualification = {
gates: ["apt-version-contract"],
version,
nativeLibaptEngine: false,
packageConversion: false,
sourceConversion: false,
};
await Bun.write(join(output, "receipt.json"), JSON.stringify(receipt, null, 2) + "\n");
console.log(JSON.stringify(receipt, null, 2));
}
if (import.meta.main)
main().catch(error => {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
});
+103
View File
@@ -0,0 +1,103 @@
#!/usr/bin/env bun
import { resolve } from "node:path";
import { parseArgs } from "node:util";
import {
assembleSources,
buildImage,
buildWasm,
packageRoot,
plan,
stageRuntime,
type Config,
type Profile,
type Target,
} from "./distribution.ts";
const usage = `Bun OS factory
plan [--profile full|perf] [--target linux-x64|linux-arm64|windows-x64|macos-arm64|wasm]
sources --out DIR [--config FILE]
stage --out DIR --runtime FILE --receipt FILE --profile full|perf --target TARGET
wasm --out DIR [--config FILE]
image --out DIR --profile full|perf --target linux-x64|linux-arm64 --tag TAG
Config: { "checkouts": { "material": "../material", "cosmic-epoch": "../cosmic" } }
Checkout paths resolve relative to the config file. Bun resolves from explicit configuration or the installed executable.
Sources contain committed, pinned files and every locked submodule; protected stores are never copied.
WASM builds libm3's portable API. Native Bun/JSC and the Linux compositor require their native targets.`;
try {
const { positionals, values } = parseArgs({
args: Bun.argv.slice(2),
allowPositionals: true,
options: {
help: { type: "boolean", short: "h" },
profile: { type: "string", default: "perf" },
target: { type: "string", default: "linux-x64" },
out: { type: "string" },
config: { type: "string" },
runtime: { type: "string" },
receipt: { type: "string" },
material: { type: "string" },
tag: { type: "string" },
},
});
if (values.help || !positionals.length) {
console.log(usage);
process.exit(0);
}
if (positionals.length !== 1) throw new Error("Expected one factory command");
if (values.profile !== "full" && values.profile !== "perf") throw new Error("Unknown distribution profile");
const profile: Profile = values.profile;
if (!["linux-x64", "linux-arm64", "windows-x64", "macos-arm64", "wasm"].includes(values.target!))
throw new Error("Unknown target");
const target = values.target as Target;
const required = (name: "out" | "runtime" | "receipt" | "material" | "tag") => {
const value = values[name];
if (!value) throw new Error(`Missing --${name}`);
return value;
};
let config: Config = {};
if (values.config) {
const file = resolve(values.config);
config = await Bun.file(file).json();
for (const [id, path] of Object.entries(config.checkouts ?? {})) config.checkouts![id] = resolve(file, "..", path);
if (config.sourceExport) {
if (config.sourceExport.cache) config.sourceExport.cache = resolve(file, "..", config.sourceExport.cache);
for (const [id, path] of Object.entries(config.sourceExport.repositories ?? {}))
config.sourceExport.repositories![id] = resolve(file, "..", path);
config.sourceExport.protectedRoots = config.sourceExport.protectedRoots?.map(path => resolve(file, "..", path));
}
}
let result: unknown;
switch (positionals[0]) {
case "plan":
result = await plan(profile, target, config);
break;
case "sources":
result = await assembleSources(resolve(required("out")), config);
break;
case "stage":
if (target === "wasm") throw new Error("Use the wasm command for the portable library target");
await stageRuntime(
resolve(required("out")),
resolve(required("runtime")),
resolve(required("receipt")),
profile,
target,
);
result = { staged: true, profile, target };
break;
case "wasm":
result = await buildWasm(resolve(required("out")), config);
break;
case "image":
result = await buildImage(resolve(required("out")), profile, target, required("tag"));
break;
default:
throw new Error(`Unknown command ${positionals[0]}; run ${packageRoot}/src/cli.ts --help`);
}
console.log(JSON.stringify(result, null, 2));
} catch (error) {
console.error(error instanceof Error ? error.message : String(error));
process.exitCode = 1;
}
+389
View File
@@ -0,0 +1,389 @@
import { exportPinnedSources, type SourceExportOptions } from "../factory/source-export.ts";
import { mkdir, lstat, copyFile, open, unlink, cp } from "node:fs/promises";
import { existsSync } from "node:fs";
import { dirname, join, resolve, relative, isAbsolute } from "node:path";
import { sourceIntegrity } from "../container/source-integrity.ts";
import { inspectLinuxABI } from "./elf.ts";
export const packageRoot = resolve(import.meta.dir, "..");
export function materialCheckout(config: Config = {}): string {
const checkout = resolve(config.checkouts?.material ?? join(packageRoot, "../material"));
if (
!existsSync(join(checkout, "crates/libm3/Cargo.toml")) ||
!existsSync(join(checkout, "crates/m3-tokens/Cargo.toml"))
)
throw new Error("Configure checkouts.material with the canonical Material Rust workspace");
return checkout;
}
export function runtimeCheckout(config: Config = {}): string {
const configured = config.checkouts?.bun ?? Bun.env.APHRODY_BUN_CHECKOUT;
if (configured) {
const root = resolve(configured);
if (!existsSync(join(root, "scripts/build.ts")))
throw new Error("Configured Bun checkout has no native build owner");
return root;
}
let root = dirname(process.execPath);
for (let depth = 0; depth < 8; depth++) {
if (existsSync(join(root, "scripts/build.ts")) && existsSync(join(root, "src/runtime/Cargo.toml"))) return root;
const parent = dirname(root);
if (parent === root) break;
root = parent;
}
throw new Error("Configure checkouts.bun or APHRODY_BUN_CHECKOUT; no runtime checkout could be resolved");
}
export type Profile = "full" | "perf";
export type Target = "linux-x64" | "linux-arm64" | "windows-x64" | "macos-arm64" | "wasm";
export interface Source {
id: string;
repository: string;
revision: string;
mount: string;
parent?: string;
submodulePath?: string;
branch?: string;
gitlinks: { path: string; revision: string; repository: string }[];
}
export interface SourceLock {
schemaVersion: 1;
repositories: Source[];
}
export interface Config {
checkouts?: Record<string, string>;
cargoCommand?: string[];
sourceExport?: Omit<SourceExportOptions, "lock" | "checkouts">;
}
function cargoOwner(config: Config): string[] {
if (config.cargoCommand?.length) return config.cargoCommand;
const checkout = runtimeCheckout(config);
const wrapper = join(checkout, "scripts/build/rust/cargo-serial.ts");
if (!existsSync(wrapper)) throw new Error("Configure the serialized native Cargo owner in cargoCommand");
return [process.execPath, wrapper];
}
export interface RuntimeReceipt {
schemaVersion: 1;
repository: "aphrody-labs/bun";
revision: string;
version: string;
target: Exclude<Target, "wasm">;
abi: "gnu" | "msvc" | "darwin";
sha256: string;
bytes: number;
qualification: { passed: boolean; gates: string[] };
}
export async function command(cmd: string[], cwd = packageRoot, timeout = 120_000): Promise<string> {
const env = { ...Bun.env };
for (const name of ["RUSTC_WRAPPER", "RUSTC_WORKSPACE_WRAPPER"]) {
const wrapper = env[name];
if (wrapper && !Bun.which(wrapper, { cwd })) {
delete env[name];
console.error(`Ignoring missing ${name} for this factory command`);
}
}
await using child = Bun.spawn({
cmd,
cwd,
env,
stdout: "pipe",
stderr: "pipe",
windowsHide: true,
timeout,
});
const [stdout, stderr, code] = await Promise.all([
new Response(child.stdout).text(),
new Response(child.stderr).text(),
child.exited,
]);
if (code !== 0) throw new Error(`${cmd[0]} failed (${code}): ${(stderr || stdout).slice(-3000)}`);
return stdout.trim();
}
function contained(root: string, path: string): string {
const result = resolve(root, path);
const rel = relative(root, result);
if (!rel || rel === ".." || rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) || isAbsolute(rel))
throw new Error(`Path must be inside the artifact root: ${path}`);
return result;
}
export async function readLock(): Promise<SourceLock> {
const lock: SourceLock = await Bun.file(join(packageRoot, "sources.lock.json")).json();
if (lock.schemaVersion !== 1 || !Array.isArray(lock.repositories) || !lock.repositories.length)
throw new Error("Invalid or empty source lock");
const seen = new Set<string>();
const mounts = new Set<string>();
for (const source of lock.repositories) {
if (!/^[\w.-]+\/[\w.-]+$/.test(source.repository) || !/^[a-f0-9]{40}$/.test(source.revision) || seen.has(source.id))
throw new Error(`Invalid source identity: ${source.id}`);
contained(packageRoot, source.mount);
if (!source.mount.startsWith("sources/") || mounts.has(source.mount))
throw new Error(`Invalid source mount: ${source.mount}`);
if (source.parent && !seen.has(source.parent)) throw new Error(`Parent must precede source: ${source.id}`);
seen.add(source.id);
mounts.add(source.mount);
}
for (const source of lock.repositories) {
if (!Array.isArray(source.gitlinks)) throw new Error(`Missing verified gitlink inventory: ${source.id}`);
const children = lock.repositories.filter(item => item.parent === source.id);
if (children.length !== source.gitlinks.length) throw new Error(`Incomplete submodule assembly: ${source.id}`);
for (const link of source.gitlinks) {
const child = children.find(item => item.submodulePath === link.path);
if (
!child ||
child.revision !== link.revision ||
child.repository !== link.repository ||
child.mount !== `${source.mount}/${link.path}`
)
throw new Error(`Unpinned submodule: ${source.id}/${link.path}`);
}
}
for (const id of ["bun", "material", "cosmic-epoch", "libcosmic"])
if (!seen.has(id)) throw new Error(`Missing distribution owner ${id}`);
return lock;
}
export async function identity(path: string) {
const stat = await lstat(path);
if (!stat.isFile() || stat.isSymbolicLink()) throw new Error(`Expected a regular artifact: ${path}`);
return contentIdentity(Bun.file(path));
}
export async function contentIdentity(blob: Blob) {
const hash = new Bun.CryptoHasher("sha256");
for await (const chunk of blob.stream()) hash.update(chunk);
return { bytes: blob.size, sha256: hash.digest("hex") };
}
export async function plan(profile: Profile, target: Target, config: Config = {}) {
const manifest = await Bun.file(join(packageRoot, "distribution.json")).json();
const sources = await readLock();
return {
...manifest,
selectedProfile: profile,
selectedTarget: target,
sourceRepositories: sources.repositories.length,
sourceRevisions: sources.repositories.map(({ id, repository, revision, mount }) => ({
id,
repository,
revision,
mount,
})),
runtimeBuild: { cwd: runtimeCheckout(config), argv: [process.execPath, "bd", "--release"] },
materialBuild: {
cwd: materialCheckout(config),
argv: [
"cargo",
"build",
"--manifest-path",
join(materialCheckout(config), "Cargo.toml"),
"-p",
"libm3",
"--release",
"--target",
manifest.targets[target].rust,
],
},
qualification: "unqualified: commands and source pins alone do not prove a distribution build",
};
}
/** Full pinned source export stays with one factory owner. */
export async function assembleSources(output: string, config: Config) {
if (!config.sourceExport?.estimates)
throw new Error("Configure sourceExport.estimates from the source-owner byte and inode inventory");
return exportPinnedSources(output, {
...config.sourceExport,
lock: await readLock(),
checkouts: { bun: runtimeCheckout(config), ...config.checkouts },
});
}
export async function stageRuntime(
output: string,
binary: string,
receiptPath: string,
profile: Profile,
target: Exclude<Target, "wasm">,
) {
const receipt: RuntimeReceipt = await Bun.file(receiptPath).json();
const lock = await readLock();
const source = lock.repositories.find(item => item.id === "bun")!;
const digest = await identity(binary);
const linux = target === "linux-x64" || target === "linux-arm64";
const expectedABI = linux ? "gnu" : target === "windows-x64" ? "msvc" : "darwin";
if (receipt.abi !== expectedABI) throw new Error(`Runtime ABI must be ${expectedABI}`);
const binaryABI = linux ? await inspectLinuxABI(binary, target === "linux-x64" ? "x64" : "arm64") : undefined;
if (
receipt.schemaVersion !== 1 ||
receipt.repository !== "aphrody-labs/bun" ||
receipt.revision !== source.revision ||
receipt.target !== target ||
receipt.version !== "1.4.4" ||
receipt.sha256 !== digest.sha256 ||
receipt.bytes !== digest.bytes ||
!receipt.qualification?.passed ||
!receipt.qualification.gates.includes("native-runtime")
)
throw new Error(
"Bun runtime must match the locked source, target, checksum and a passed native-runtime qualification",
);
const sourcesReceipt = await Bun.file(join(output, "sources.receipt.json")).json();
if (
!sourcesReceipt.complete ||
JSON.stringify(sourcesReceipt.repositories.map((s: Source) => [s.id, s.revision])) !==
JSON.stringify(lock.repositories.map(s => [s.id, s.revision]))
)
throw new Error("Stage requires a complete source assembly matching the source lock");
const contents = await sourceIntegrity(join(output, "sources"));
if (contents.treeSHA256 !== sourcesReceipt.treeSHA256)
throw new Error("Assembled source content changed after export");
await mkdir(join(output, "runtime"), { recursive: true });
await copyFile(binary, join(output, "runtime", target === "windows-x64" ? "bun.exe" : "bun"));
await Bun.write(join(output, "runtime", "receipt.json"), JSON.stringify(receipt, null, 2) + "\n");
const manifest = await Bun.file(join(packageRoot, "distribution.json")).json();
await Bun.write(
join(output, "distribution.json"),
JSON.stringify(
{
...manifest,
profile,
target,
runtime: receipt,
binaryABI,
qualification: "runtime-qualified; desktop and image remain unqualified",
},
null,
2,
) + "\n",
);
await copyFile(join(packageRoot, "container", "Dockerfile"), join(output, "Dockerfile"));
await copyFile(join(packageRoot, "container", ".dockerignore"), join(output, ".dockerignore"));
await cp(join(packageRoot, "container"), join(output, "container"), {
recursive: true,
errorOnExist: true,
force: false,
});
await mkdir(join(output, "wasm"), { recursive: true });
}
export async function buildWasm(output: string, config: Config) {
const material = materialCheckout(config);
const claim = await open(join(packageRoot, ".factory.lock"), "wx");
await claim.writeFile(`${process.pid}\n`);
try {
const targetDir = resolve(output, ".cargo-wasm");
await command(
[
...cargoOwner(config),
"build",
"--locked",
"--manifest-path",
join(material, "Cargo.toml"),
"-p",
"libm3-wasm",
"--release",
"--target",
"wasm32-unknown-unknown",
"--target-dir",
targetDir,
"-j",
"2",
],
material,
1_800_000,
);
const artifact = join(targetDir, "wasm32-unknown-unknown/release/libm3_wasm.wasm");
const digest = await identity(artifact);
const magic = await Bun.file(artifact).slice(0, 8).bytes();
if (magic.length !== 8 || magic[0] !== 0 || magic[1] !== 97 || magic[2] !== 115 || magic[3] !== 109)
throw new Error("Missing valid WASM artifact");
await mkdir(join(output, "wasm"), { recursive: true });
await copyFile(artifact, join(output, "wasm/libm3.wasm"));
const sourceFiles = [];
for (const path of ["Cargo.toml", "Cargo.lock"])
sourceFiles.push({ path, ...(await identity(join(material, path))) });
for await (const path of new Bun.Glob("crates/{m3-tokens,m3,libm3,libm3-wasm}/**/*.{rs,toml}").scan({
cwd: material,
onlyFiles: true,
}))
sourceFiles.push({ path: path.replaceAll("\\", "/"), ...(await identity(join(material, path))) });
sourceFiles.sort((a, b) => a.path.localeCompare(b.path));
const receipt = {
schemaVersion: 1,
kind: "libm3-wasm",
sourceRepository: "aphrody-labs/material",
sourceRevision: await command(["git", "rev-parse", "HEAD"], material),
sourceDirty: Boolean(
await command(["git", "status", "--porcelain", "--", "Cargo.toml", "Cargo.lock", "crates"], material),
),
sourceFiles,
target: "wasm32-unknown-unknown",
compiler: await command(["rustc", "--version"]),
...digest,
bunRuntimeWasm: false,
};
await Bun.write(join(output, "wasm/receipt.json"), JSON.stringify(receipt, null, 2) + "\n");
return receipt;
} finally {
await claim.close();
await unlink(join(packageRoot, ".factory.lock"));
}
}
export async function buildImage(output: string, profile: Profile, target: Target, tag: string) {
if (target !== "linux-x64" && target !== "linux-arm64") throw new Error("Docker images require a Linux target");
if (target === "linux-arm64")
throw new Error("The COSMIC package lock currently covers amd64; ARM64 needs its own verified package inventory");
const receipt = await Bun.file(join(output, "distribution.json")).json();
if (receipt.profile !== profile || receipt.target !== target)
throw new Error("Image profile and target must match the staged artifact");
if (!/^[a-z0-9][a-z0-9._/:@-]+$/.test(tag)) throw new Error("Invalid OCI image tag");
await command(
[
"docker",
"buildx",
"build",
"--load",
"--platform",
target === "linux-x64" ? "linux/amd64" : "linux/arm64",
"--target",
profile,
"--tag",
tag,
"--build-arg",
`SOURCE_REVISION=${receipt.runtime.revision}`,
"--build-arg",
`DISTRIBUTION_VERSION=${receipt.version}`,
resolve(output),
],
packageRoot,
3_600_000,
);
const image = JSON.parse(await command(["docker", "image", "inspect", tag]));
const smoke = await command([
"docker",
"run",
"--rm",
"--network=none",
"--entrypoint",
"/usr/local/bin/bun",
tag,
"--version",
]);
if (smoke !== "1.4.4") throw new Error(`Image Bun version mismatch: ${smoke}`);
const result = {
schemaVersion: 1,
kind: "bun-os-image",
profile,
target,
tag,
imageId: image[0].Id,
bytes: image[0].Size,
runtimeSmoke: smoke,
desktopQualified: false,
};
await Bun.write(join(output, "image.receipt.json"), JSON.stringify(result, null, 2) + "\n");
return result;
}
+72
View File
@@ -0,0 +1,72 @@
export interface LinuxABI {
abi: "gnu";
libc: "glibc";
interpreter: string;
machine: number;
}
/** Read the loader selected by the artifact itself before staging it on a GNU host. */
export async function inspectLinuxABI(path: string, architecture: "x64" | "arm64"): Promise<LinuxABI> {
const file = Bun.file(path);
const header = await file.slice(0, 64).bytes();
if (
header.length !== 64 ||
header[0] !== 127 ||
header[1] !== 69 ||
header[2] !== 76 ||
header[3] !== 70 ||
header[4] !== 2 ||
header[5] !== 1
)
throw new Error("Linux artifacts must be 64-bit little-endian ELF executables");
const view = new DataView(header.buffer, header.byteOffset, header.byteLength);
const machine = view.getUint16(18, true);
if (machine !== (architecture === "x64" ? 62 : 183))
throw new Error("ELF machine differs from the selected architecture");
const offset = Number(view.getBigUint64(32, true));
const entrySize = view.getUint16(54, true);
const count = view.getUint16(56, true);
if (
!Number.isSafeInteger(offset) ||
offset < 64 ||
offset > 2 * 1024 * 1024 ||
entrySize < 56 ||
entrySize > 256 ||
count < 1 ||
count > 128 ||
offset + entrySize * count > file.size
)
throw new Error("Invalid ELF program header table");
const bytes = await file.slice(offset, offset + entrySize * count).bytes();
const programs = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
let interpreter: string | undefined;
for (let index = 0; index < count; index++) {
const at = index * entrySize;
if (programs.getUint32(at, true) !== 3) continue;
if (interpreter) throw new Error("ELF contains multiple interpreter declarations");
const start = Number(programs.getBigUint64(at + 8, true));
const length = Number(programs.getBigUint64(at + 32, true));
if (
!Number.isSafeInteger(start) ||
!Number.isSafeInteger(length) ||
start < 0 ||
length < 2 ||
length > 256 ||
start + length > file.size
)
throw new Error("Invalid ELF interpreter range");
const value = await file.slice(start, start + length).bytes();
if (value[value.length - 1] !== 0 || value.subarray(0, -1).includes(0))
throw new Error("Invalid ELF interpreter name");
interpreter = new TextDecoder().decode(value.subarray(0, -1));
}
const expected =
architecture === "x64"
? ["/lib64/ld-linux-x86-64.so.2", "/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2"]
: ["/lib/ld-linux-aarch64.so.1", "/lib/aarch64-linux-gnu/ld-linux-aarch64.so.1"];
if (!interpreter || !expected.includes(interpreter))
throw new Error(
"The COSMIC distribution requires the GNU glibc loader; static or musl artifacts need a separate target",
);
return { abi: "gnu", libc: "glibc", interpreter, machine };
}
+21
View File
@@ -0,0 +1,21 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "Preserve",
"moduleResolution": "Bundler",
"moduleDetection": "force",
"verbatimModuleSyntax": true,
"strict": true,
"noEmit": true,
"allowImportingTsExtensions": true,
"skipLibCheck": true,
"types": ["@aphrody/bun-types"]
},
"include": [
"src/**/*.ts",
"container/**/*.ts",
"factory/**/*.ts",
"packages/package-converter/src/**/*.ts",
"infra/**/*.ts"
]
}