mirror of
https://github.com/aphrody-labs/bun-distribution.git
synced 2026-10-13 23:56:39 +00:00
Establish unified Bun OS factory, app builds and package conversion
This commit is contained in:
commit
2e413aa6d9
65 files changed
+11621
No files matched your search
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
/out/
|
||||
/*.local.json
|
||||
/.factory.lock
|
||||
/node_modules/
|
||||
/target/
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"printWidth": 120,
|
||||
"trailingComma": "all",
|
||||
"useTabs": false,
|
||||
"quoteProps": "preserve"
|
||||
}
|
||||
@@ -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/**
|
||||
@@ -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"]
|
||||
@@ -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).
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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}`);
|
||||
@@ -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") };
|
||||
}
|
||||
@@ -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" }));
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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.
|
||||
@@ -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/).
|
||||
@@ -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."
|
||||
]
|
||||
}
|
||||
@@ -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/).
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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).
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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
@@ -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;
|
||||
});
|
||||
@@ -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
@@ -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
@@ -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;
|
||||
});
|
||||
@@ -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}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
});
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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).
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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" };
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -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",
|
||||
]);
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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("/");
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"moduleDetection": "force",
|
||||
"verbatimModuleSyntax": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
+1118
File diff suppressed because it is too large.
Load diff
+52
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
@@ -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
@@ -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 };
|
||||
}
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user