diff --git a/.github/workflows/rust-sdk-check.yml b/.github/workflows/rust-sdk-check.yml new file mode 100644 index 0000000..4fb4ab6 --- /dev/null +++ b/.github/workflows/rust-sdk-check.yml @@ -0,0 +1,35 @@ +name: Check standalone Rust application SDK +on: + push: + branches: [staging, main, master] + paths: ['rust-sdk/**', 'actions/setup-rust-sdk/**', '.github/workflows/rust-sdk-check.yml'] + pull_request: + paths: ['rust-sdk/**', 'actions/setup-rust-sdk/**', '.github/workflows/rust-sdk-check.yml'] +permissions: + contents: read +jobs: + check: + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - run: python3 -m unittest discover -s rust-sdk/tests -v + signer: + runs-on: ubuntu-24.04 + timeout-minutes: 30 + env: + CARGO_BUILD_JOBS: '4' + steps: + - uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + with: + persist-credentials: false + - id: sdk + uses: ./actions/setup-rust-sdk + with: + output: ${{ runner.temp }}/mochios-rust-sdk + - name: Test and build the trusted signer without application sources or credentials + env: + SDK: ${{ steps.sdk.outputs.sdk }} + HELPER: ${{ steps.sdk.outputs.helper }} + run: python3 "$(dirname "$HELPER")/host-tool.py" --sdk "$SDK" --tool msign --output "$RUNNER_TEMP/host-tools" --test diff --git a/actions/setup-rust-sdk/action.yml b/actions/setup-rust-sdk/action.yml new file mode 100644 index 0000000..27e7124 --- /dev/null +++ b/actions/setup-rust-sdk/action.yml @@ -0,0 +1,23 @@ +name: Set up mochiOS Rust application SDK +description: Install a pinned C SDK through komeup and prepare Rust, AppCore and ViewKit without an OS checkout. +inputs: + output: + description: Fresh directory for the isolated SDK (prefer runner.temp). + required: true +outputs: + sdk: + description: Local app-build.json, for this runner only; do not publish it. + value: ${{ steps.setup.outputs.sdk }} + helper: + description: Path to the generic Rust app build/test/staging helper. + value: ${{ steps.setup.outputs.helper }} +runs: + using: composite + steps: + - id: setup + shell: bash + env: + SDK_OUTPUT: ${{ inputs.output }} + SDK_ACTION: ${{ github.action_path }} + SDK_ACTION_REF: ${{ github.action_ref }} + run: python3 "$SDK_ACTION/../../rust-sdk/setup.py" --output "$SDK_OUTPUT" diff --git a/rust-sdk/README.md b/rust-sdk/README.md new file mode 100644 index 0000000..18d979e --- /dev/null +++ b/rust-sdk/README.md @@ -0,0 +1,84 @@ +# Standalone Rust application builds + +`actions/setup-rust-sdk` prepares a native Rust application build environment on +Ubuntu 24.04. It installs the explicit released C/C++ SDK through komeup, verifies +the reviewed archive SHA-256, and fetches the fixed Rust std, ViewKit, font and +protocol sources from `sources.lock.json`. AppCore comes from the same DevKit +revision as the Action. No OS checkout, image build, SDK release, credential or +organization membership is required. + +This initial source SDK prepares one application at a time. It uses the existing +mochiOS Rust std fork and pinned nightly compiler. It is not a newly published +binary Rust SDK. The runner-local `app-build.json` contains local paths and must +not be uploaded or shipped. Immutable source pins and font hashes are public; +the installation does not change the developer's default komeup/rustup choices. + +Example caller (replace `staging` with a reviewed full commit SHA): + +```yaml +- id: sdk + uses: mochiOS/devkit/actions/setup-rust-sdk@staging + with: + output: ${{ runner.temp }}/mochios-rust-sdk +- name: Test, compile and stage + env: + HELPER: ${{ steps.sdk.outputs.helper }} + SDK: ${{ steps.sdk.outputs.sdk }} + run: python3 "$HELPER" --source "$GITHUB_WORKSPACE" --sdk "$SDK" --output "$RUNNER_TEMP/app-build" --test +- id: package + uses: mochiOS/devkit/actions/build-mpkg@20d3fb71b952ae7a4ff583dafe8cc3d00e24d3a3 + with: + manifest: ${{ runner.temp }}/app-build/stage/manifest.toml + payload: ${{ runner.temp }}/app-build/stage/payload + output: dist/Example.mpkg +``` + +Compilation and native host unit tests run in an isolated staged Cargo project. +The helper maps the existing AppCore/protocol source dependencies into the SDK, +preserves the application's lockfile as input, and exports its actual resulting +resolution as `resolved.Cargo.lock` for review. It never edits or rolls back the +source checkout's Cargo.lock. SDK changes can require adopting this resolution +into the application repository before production release. + +The staging helper supports a single-entry Rust application with a completed +MPKG manifest. It preserves document associations and requested capabilities, +copies declared resources, binds real file hashes/modes, removes source-tree +built-in provenance, and changes the binary destination to the normal +`/applications/.app` package path. ELF and host-path checks do not execute +target programs and do not establish native runtime success. The public MPKG +Action separately creates the unsigned container. Signing, approval and release +publication remain caller workflow responsibilities. + +## Approval-protected publication + +The generic `release.py` helper consumes the unsigned artifact from the same +successful workflow run. It checks the source commit, package ID, actual MPKG +manifest, SDK source lock and archive checksum before accessing signing inputs. +It does not compile or execute application code. `host-tool.py` builds msign from +the same trusted SDK source revision, with developer-CA networking disabled. + +The caller must place its publication job behind a protected GitHub Environment. +Configure the repository's chosen reviewer/team and restrict the environment to +its default branch. Pass `--environment`, `--reviewer-team` (numeric Team ID), +and `--branch` to the release helper. It requires exactly that Team as reviewer, +admin bypass disabled, and exactly that branch allowed; unreadable or changed +policy aborts publication. This verifies configuration, while GitHub's protected +job enforces the approval itself. The public packaging/SDK Actions have no organization or +team requirement of their own. + +Official app callers use an `app-release` Environment with +`application-maintainers` approval. Releases stay disabled until the repository +variable `APP_RELEASE_ENABLED` is explicitly set to `true` and these Environment +values have been configured: + +* Secret `MSIGN_CERTIFICATE_B64`: base64 of the issued Developer Certificate file. +* Secret `MSIGN_PRIVATE_KEY_B64`: base64 of its corresponding private-key file. +* Variable `MSIGN_ROOT_PUBLIC_KEY_B64`: base64 of the trusted issuer public-key file. + +No root private key, fixture key, generated substitute identity or unsigned +publication is accepted. Credentials are decoded into a private temporary +directory, not propagated as subprocess environment values, and removed on +success or failure. The signed package is verified against the trusted root +before publication. Existing release tags/assets are never replaced. The initial +release helper publishes prereleases as `v-r`; update the +manifest revision before making another release of the same version. diff --git a/rust-sdk/build.py b/rust-sdk/build.py new file mode 100644 index 0000000..7790fb2 --- /dev/null +++ b/rust-sdk/build.py @@ -0,0 +1,155 @@ +#!/usr/bin/env python3 +"""Build/test a Rust app and stage an unsigned package for the public MPKG Action.""" +import argparse +import copy +import hashlib +import json +import os +from pathlib import Path, PurePosixPath +import re +import shutil +import struct +import subprocess +import tomllib + + +def run(args, **kwargs): + """Preserve real compiler and test failures with streaming build output.""" + subprocess.run([str(a) for a in args], check=True, **kwargs) + + +def dumps(data): + """Write ordinary Cargo/MPKG tables, preserving nested metadata and array tables.""" + lines = [] + def table(mapping, path=(), header=None): + if header: + lines.append(header) + children = [] + for key, value in mapping.items(): + if isinstance(value, dict) or (isinstance(value, list) and value and isinstance(value[0], dict)): + children.append((key, value)) + else: + lines.append(json.dumps(key) + ' = ' + json.dumps(value, ensure_ascii=True, allow_nan=False)) + for key, value in children: + child = (*path, key) + name = '.'.join(json.dumps(p) for p in child) + for item in value if isinstance(value, list) else [value]: + lines.append('') + table(item, child, '[[' + name + ']]' if isinstance(value, list) else '[' + name + ']') + table(data) + return '\n'.join(lines) + '\n' + + +def resource(source, name): + """Find a declared bundle-local resource without following links outside source.""" + path = PurePosixPath(name) + if not name or path.is_absolute() or '..' in path.parts or '\\' in name: + raise ValueError('Invalid resource path: ' + name) + for candidate in (source / name, source / 'resources' / name): + if candidate.is_file() and candidate.resolve().is_relative_to(source.resolve()): + return candidate + raise ValueError('Missing or external resource: ' + name) + + +def stage(source, binary, output, licenses=None): + """Preserve capabilities/associations and bind actual files without claiming built-in trust.""" + with binary.open('rb') as stream: + header = stream.read(64) + if len(header) != 64 or header[:6] != b'\x7fELF\x02\x01' or struct.unpack_from(' 1024 * 1024 or checksums.stat().st_size > 4096: + raise ValueError('Artifact metadata exceeds the size limit') + record = json.loads(metadata.read_text()) + lock = json.loads((HERE / 'sources.lock.json').read_text()) + if (record.get('format') != 1 or record.get('source_commit') != expected_commit + or record.get('signed') is not False or record.get('sources') != lock or record.get('devkit_ref') != expected_sdk_ref): + raise ValueError('Artifact source/SDK does not match the approved release') + if checksums.read_text() != digest(package) + ' ' + package.name + '\n': + raise ValueError('Unsigned artifact checksum mismatch') + with package.open('rb') as stream: + header = stream.read(32) + if len(header) != 32 or header[:12] != b'MPKG\x01\0\0\0\x20\0\0\0': + raise ValueError('Expected unsigned MPKG v1') + with tarfile.open(fileobj=stream, mode='r|') as archive: + first = next(iter(archive)) + if first.name != 'manifest.toml' or not first.isfile() or first.size > 1024 * 1024: + raise ValueError('Invalid MPKG manifest') + manifest = tomllib.loads(archive.extractfile(first).read().decode('utf-8')) + identity = manifest.get('package', {}) + if identity != record.get('package') or identity.get('id') != expected_id or 'provenance' in identity: + raise ValueError('MPKG identity differs from the approved application') + version, revision = identity.get('version'), identity.get('revision') + if (not isinstance(version, str) or not re.fullmatch('[0-9][0-9A-Za-z.+-]*', version) + or type(revision) is not int or revision < 1): + raise ValueError('Invalid release version/revision') + return package, identity, 'v' + version + '-r' + str(revision) + + +def sign(package, output, msign): + """Use explicit environment credentials, verify the result, and remove temporary keys.""" + required = ('MSIGN_CERTIFICATE_B64', 'MSIGN_PRIVATE_KEY_B64', 'MSIGN_ROOT_PUBLIC_KEY_B64') + if any(not os.environ.get(key) for key in required): + raise ValueError('Signing certificate, private key, and trusted root public key must be configured before enabling releases') + with tempfile.TemporaryDirectory(prefix='mpkg-signing-') as temporary: + root = Path(temporary) + root.chmod(0o700) + paths = [] + for key in required: + data = base64.b64decode(os.environ[key], validate=True) + if not data or len(data) > 1024 * 1024: + raise ValueError('Invalid signing credential file') + path = root / key + with path.open('xb') as stream: + path.chmod(0o600) + stream.write(data) + paths.append(path) + certificate, private, issuer = paths + env = dict(os.environ, MOCHIOS_SIGNING_HOME=str(root / 'identity')) + # Do not propagate the credential values to subprocess environments. + for key in required: + env.pop(key, None) + for key in ('GH_TOKEN', 'GITHUB_TOKEN'): + env.pop(key, None) + subprocess.run([str(msign), 'identity', 'import', 'ci-release', '--certificate', str(certificate), '--key', str(private)], env=env, check=True) + subprocess.run([str(msign), 'package', 'sign-auto', str(package), '--identity', 'ci-release', '--output', str(output)], env=env, check=True) + subprocess.run([str(msign), 'package', 'verify', str(output), '--root-public-key', str(issuer), '--unix-time', str(int(time.time()))], env=env, check=True) + + +def absent(repository, path): + """Require a definite 404; authentication/network failures are never interpreted as absence.""" + result = subprocess.run(['gh', 'api', '-i', 'repos/' + repository + '/' + path], capture_output=True, text=True) + if result.returncode == 0 or 'HTTP 404' not in result.stderr: + raise ValueError('Release/tag already exists or could not be checked; refusing to overwrite') + + +def approval_policy(repository, environment, team, branch): + """Fail closed if the caller's required Team or branch restriction was removed.""" + endpoint = 'repos/' + repository + '/environments/' + quote(environment, safe='') + def fetch(path): + return json.loads(subprocess.check_output(['gh', 'api', path], text=True)) + policy = fetch(endpoint) + reviewers = [reviewer for rule in policy.get('protection_rules', []) + if rule.get('type') == 'required_reviewers' for reviewer in rule.get('reviewers', [])] + # GitHub accepts approval from any listed reviewer. Do not allow a different + # user/team to substitute for the Team explicitly selected by the publisher. + if (policy.get('can_admins_bypass') is not False or len(reviewers) != 1 + or reviewers[0].get('type') != 'Team' or reviewers[0].get('reviewer', {}).get('id') != team + or policy.get('deployment_branch_policy') != dict(protected_branches=False, custom_branch_policies=True)): + raise ValueError('Required Team approval or environment restrictions are missing') + branches = fetch(endpoint + '/deployment-branch-policies?per_page=100') + entries = branches.get('branch_policies', []) + if (branches.get('total_count') != 1 or len(entries) != 1 + or entries[0].get('name') != branch or entries[0].get('type') != 'branch'): + raise ValueError('Publication environment must allow only the selected branch') + + +def main(): + """Only an approval-protected caller should execute this publication command.""" + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--artifacts', type=Path, required=True) + parser.add_argument('--msign', type=Path, required=True) + parser.add_argument('--package-id', required=True) + parser.add_argument('--commit', required=True) + parser.add_argument('--sdk-ref', required=True) + parser.add_argument('--repository', required=True) + parser.add_argument('--environment', required=True) + parser.add_argument('--reviewer-team', type=int, required=True) + parser.add_argument('--branch', required=True) + args = parser.parse_args() + if not re.fullmatch('[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+', args.repository): + raise ValueError('Invalid release repository') + package, identity, tag = validate(args.artifacts.resolve(), args.package_id, args.commit, args.sdk_ref) + approval_policy(args.repository, args.environment, args.reviewer_team, args.branch) + absent(args.repository, 'releases/tags/' + tag) + absent(args.repository, 'git/ref/tags/' + tag) + with tempfile.TemporaryDirectory(prefix='mpkg-release-') as temporary: + root = Path(temporary) + output = root / package.name + sign(package, output, args.msign.resolve()) + checksums = root / 'SHA256SUMS' + checksums.write_text(digest(output) + ' ' + output.name + '\n') + notes = root / 'notes.md' + notes.write_text('Native mochiOS application package.\n\nSource commit: `' + args.commit + '`\n') + subprocess.run(['gh', 'release', 'create', tag, str(output), str(checksums), '--repo', args.repository, + '--target', args.commit, '--title', identity['name'] + ' ' + identity['version'] + ' (r' + str(identity['revision']) + ')', + '--prerelease', '--notes-file', str(notes)], check=True) + + +if __name__ == '__main__': + main() diff --git a/rust-sdk/setup.py b/rust-sdk/setup.py new file mode 100644 index 0000000..503d815 --- /dev/null +++ b/rust-sdk/setup.py @@ -0,0 +1,148 @@ +#!/usr/bin/env python3 +"""Compose a Rust application SDK from source pins and a released SDK, without an OS checkout.""" +import argparse +import hashlib +import json +import os +from pathlib import Path +import re +import shlex +import shutil +import subprocess +import tomllib + +HERE = Path(__file__).resolve().parent + + +def run(*args, **kwargs): + """Stream real tool diagnostics and propagate failures.""" + subprocess.run([str(a) for a in args], check=True, **kwargs) + + +def checkout(root, name, spec): + """Fetch a full reviewed commit, never a mutable branch or submodule graph.""" + if not re.fullmatch(r'[0-9a-f]{40}', spec['revision']): + raise ValueError('Expected a full source commit ID') + target = root / name + target.mkdir(parents=True) + run('git', 'init', target) + run('git', '-C', target, 'remote', 'add', 'origin', 'https://github.com/' + spec['repository'] + '.git') + if name == 'libraries/rust': + run('git', '-C', target, 'sparse-checkout', 'set', 'library', 'src/mochios-backtrace', + 'src/mochios-libunwind', 'vendor/rustc-literal-escaper') + run('git', '-C', target, 'fetch', '--depth=1', '--filter=blob:none', 'origin', spec['revision']) + run('git', '-C', target, 'checkout', '--detach', spec['revision']) + if subprocess.check_output(['git', '-C', str(target), 'rev-parse', 'HEAD'], text=True).strip() != spec['revision']: + raise ValueError('Source revision mismatch') + + +def configs(root): + """Use a coherent source graph for standard-library, framework and protocol crates.""" + patches = {'crates-io': {'libc': root / 'libraries/libc', 'viewkit': root / 'libraries/viewkit'}, + 'https://github.com/mochiOS/libc': {'libc': root / 'libraries/libc'}, + 'https://github.com/mochiOS/viewkit': {'viewkit': root / 'libraries/viewkit'}, + 'https://github.com/mochiOS/mnu': {'mnu-abi': root / 'core/crates/abi'}, + 'https://github.com/mochiOS/mBoot': {'mboot-protocol': root / 'services/mboot-protocol'}} + patches['https://github.com/mochiOS/syscalls'] = { + tomllib.loads(p.read_text())['package']['name']: p.parent for p in (root / 'user/crates').glob('*/Cargo.toml')} + for name in ('core', 'alloc', 'std'): + patches['crates-io']['rustc-std-workspace-' + name] = root / 'rust-overlay/lib/rustlib/src/rust/library' / ('rustc-std-workspace-' + name) + return ['patch.' + json.dumps(source) + '.' + json.dumps(name) + '.path=' + json.dumps(str(path)) + for source, crates in patches.items() for name, path in crates.items()] + + +def overlay(root, toolchain): + """Pair the known Rust compiler with mochiOS std, preserving the installed rustup tree.""" + run('rustup', 'toolchain', 'install', toolchain, '--profile', 'minimal', '--component', 'rust-src') + base = Path(subprocess.check_output(['rustc', '+' + toolchain, '--print', 'sysroot'], text=True).strip()) + output = root / 'rust-overlay' + (output / 'bin').mkdir(parents=True) + library = output / 'lib/rustlib/src/rust/library' + library.mkdir(parents=True) + for path in (base / 'bin').iterdir(): + dest = output / 'bin' / path.name + if path.name in ('rustc', 'rustdoc'): + dest.write_text('#!/bin/sh\nexec ' + shlex.quote(str(path)) + ' --sysroot ' + shlex.quote(str(output)) + ' "$@"\n') + dest.chmod(0o755) + else: + dest.symlink_to(path) + for path in (base / 'lib').iterdir(): + if path.name != 'rustlib': + (output / 'lib' / path.name).symlink_to(path) + for path in (base / 'lib/rustlib').iterdir(): + if path.name != 'src': + (output / 'lib/rustlib' / path.name).symlink_to(path) + source = root / 'libraries/rust' + for path in (source / 'library').iterdir(): + if path.name not in ('backtrace', 'vendor'): + if path.name == 'std': + shutil.copytree(path, library / 'std', copy_function=lambda src, dst: os.symlink(src, dst)) + else: + (library / path.name).symlink_to(path) + (library / 'backtrace').symlink_to(source / 'src/mochios-backtrace') + unwind = output / 'lib/rustlib/src/rust/src/llvm-project' + unwind.mkdir(parents=True) + (unwind / 'libunwind').symlink_to(source / 'src/mochios-libunwind') + for vendor in (library / 'vendor', output / 'lib/rustlib/src/rust/vendor'): + vendor.mkdir() + (vendor / 'rustc-literal-escaper').symlink_to(source / 'vendor/rustc-literal-escaper') + return output, base + + +def prepare(output): + """Verify/install the C SDK through komeup, then prepare independent Rust metadata.""" + root = output.resolve() + if root.exists(): + raise ValueError('Use a fresh SDK output directory') + root.mkdir(parents=True) + lock = json.loads((HERE / 'sources.lock.json').read_text()) + revision = os.environ.get('SDK_ACTION_REF') or subprocess.check_output( + ['git', '-C', str(HERE.parent), 'rev-parse', 'HEAD'], text=True).strip() + for name, spec in lock['sources'].items(): + checkout(root, name, spec) + devkit = root / 'tools/devkit' + devkit.parent.mkdir() + shutil.copytree(HERE.parent, devkit, ignore=shutil.ignore_patterns('.git', 'target', 'out', '__pycache__')) + tool = root / 'komeup-build' + tool.mkdir() + shutil.copyfile(HERE.parent / 'crates/komeup/Cargo.toml', tool / 'Cargo.toml') + shutil.copyfile(HERE / 'komeup.Cargo.lock', tool / 'Cargo.lock') + shutil.copytree(HERE.parent / 'crates/komeup/src', tool / 'src') + with (tool / 'Cargo.toml').open('a') as stream: + stream.write('\n[workspace]\n') + run('cargo', 'build', '--locked', '--release', '--manifest-path', tool / 'Cargo.toml', '--target-dir', root / 'host-tools') + komeup = root / 'host-tools/release/komeup' + env = dict(os.environ, KOMEUP_HOME=str(root / 'manager')) + run(komeup, 'sdk', 'install', lock['sdk']['version'], env=env) + sdk = Path(subprocess.check_output([str(komeup), 'sdk', 'path', '--version', lock['sdk']['version']], env=env, text=True).strip()) + receipt = tomllib.loads((sdk / '.komeup-sdk.toml').read_text()) + if receipt['sha256'] != lock['sdk']['sha256']: + raise ValueError('SDK archive differs from the reviewed SHA-256') + compiler, base = overlay(root, lock['rust_toolchain']) + fonts = root / 'libraries/fonts' + run('perl', fonts / 'scripts/install-fonts.pl', '--config', fonts / 'fonts.conf', '--output', fonts / 'out/fonts', + '--cache', fonts / 'out/cache', '--work', fonts / 'out/build', '--curl', 'curl') + for name, expected in lock['fonts'].items(): + if hashlib.sha256((fonts / 'out/fonts' / name).read_bytes()).hexdigest() != expected: + raise ValueError('Font hash mismatch: ' + name) + flags = ['--remap-path-prefix=' + str(Path.home()) + '=/build', '--remap-path-prefix=' + str(base) + '=/toolchain', + '--remap-path-prefix=' + str(root) + '=/build/sdk', '-C', 'debuginfo=0', '-C', 'linker=' + str(sdk / 'bin/mochios-cc')] + metadata = dict(format=1, devkit_ref=revision, cargo=str(compiler / 'bin/cargo'), rustc=str(compiler / 'bin/rustc'), + rustdoc=str(compiler / 'bin/rustdoc'), target=str(root / 'user/targets/x86_64-unknown-mochios.json'), + target_dir=str(root / 'target'), rustflags=flags, cargo_config=configs(root), + appcore=str(devkit / 'crates/appcore'), user=str(root / 'user'), + font_licenses=str(fonts / 'out/fonts/licenses'), sources=lock, + fonts={key: str(fonts / 'out/fonts' / name) for key, name in [ + ('VIEWKIT_UI_FONT_PATH', 'InterVariable.ttf'), ('VIEWKIT_MONOSPACE_FONT_PATH', 'UDEVGothic-Regular.ttf'), + ('VIEWKIT_JAPANESE_FONT_PATH', 'IBMPlexSansJP-Regular.ttf')]}) + description = root / 'app-build.json' + description.write_text(json.dumps(metadata, indent=2) + '\n') + if os.environ.get('GITHUB_OUTPUT'): + with open(os.environ['GITHUB_OUTPUT'], 'a') as stream: + stream.write('sdk=' + str(description) + '\nhelper=' + str(HERE / 'build.py') + '\n') + + +if __name__ == '__main__': + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument('--output', type=Path, required=True) + prepare(parser.parse_args().output) diff --git a/rust-sdk/sources.lock.json b/rust-sdk/sources.lock.json new file mode 100644 index 0000000..70158db --- /dev/null +++ b/rust-sdk/sources.lock.json @@ -0,0 +1,46 @@ +{ + "format": 1, + "rust_toolchain": "nightly-2026-07-16", + "sdk": { + "version": "27.0-dp.1-shared.8", + "sha256": "3a4b09efd45d1ced90d5e7ce137dc2cb5e206797847c86dedfc93719b95b3755" + }, + "sources": { + "user": { + "repository": "mochiOS/syscalls", + "revision": "f585ece9c9b331040707da5bae9a2796eadf1a93" + }, + "core": { + "repository": "mochiOS/mnu", + "revision": "1cd3f9f694162b07140f9d51ca038ec412c1ad0f" + }, + "services": { + "repository": "mochiOS/services", + "revision": "2c756be6014dace6cdba0f8be86efadef255370c" + }, + "libraries/rust": { + "repository": "mochiOS/rust", + "revision": "a53123d944398a09672dd3633e6bed8d1375792e" + }, + "libraries/viewkit": { + "repository": "mochiOS/ViewKit", + "revision": "21c7a55254b4f624e9bc2796972d2fe0369ebdd9" + }, + "libraries/fonts": { + "repository": "mochiOS/fonts", + "revision": "233cb947ed6b10e175b0c6570611eb5605f112e1" + }, + "libraries/libc": { + "repository": "mochiOS/libc", + "revision": "a3c482e222b53700a09d21377633836e2fee07be" + } + }, + "fonts": { + "UDEVGothic-Regular.ttf": "5a4349e4a54f18a3b9691cba803c10925f277bc0cd27a5644597836be97a2e86", + "IBMPlexSansJP-SemiBold.ttf": "4f6a9cf3957c407f0913cf3eb251c88cd051ff1d976f4474b39f149100443f22", + "IBMPlexSansJP-Regular.ttf": "25d96fe620f12fba6cf09158807117ec13d1e7c9debff488117cf21dc8844688", + "UDEVGothic-Bold.ttf": "fd8f22c8a70796c7853bc34592a1f7affb7b7f25a471cd863d78ef0bfc377587", + "InterVariable.ttf": "4989b125924991b90d05b2d16e0e388c48f7d5bb8b30539bbf9c755278d0ccaf", + "IBMPlexSansJP-Medium.ttf": "ad2ecca35612dd6637ebbe84fd590bdd8e007957bec17f67a6f5519f64958552" + } +} diff --git a/rust-sdk/tests/test_build.py b/rust-sdk/tests/test_build.py new file mode 100644 index 0000000..465d9e3 --- /dev/null +++ b/rust-sdk/tests/test_build.py @@ -0,0 +1,71 @@ +import importlib.util +import json +from pathlib import Path +import struct +import tempfile +import tomllib +import unittest + +HERE = Path(__file__).resolve().parents[1] +spec = importlib.util.spec_from_file_location('build_app', HERE / 'build.py') +build = importlib.util.module_from_spec(spec) +spec.loader.exec_module(build) + + +class PackageStage(unittest.TestCase): + def setUp(self): + self.tmp = tempfile.TemporaryDirectory() + self.addCleanup(self.tmp.cleanup) + self.root = Path(self.tmp.name) + self.source = self.root / 'independent-app' + self.source.mkdir() + manifest = dict(format=1, package=dict(id='net.example.editor', name='Editor', provenance='built-in', + version='1.0.0', revision=1, vendor='Independent developer', kind='application', architecture='x86_64', abi='mochios-1'), + application=dict(entry='entry.elf', icon='icon.svg', resources=['icon.svg'], + document_extensions=['txt'], document_icons=[dict(extensions=['txt'], resource='icon.svg')]), + binary=[dict(path='/system/applications/Editor.app/entry.elf', kind='application', requires=['window.create'])]) + (self.source / 'manifest.toml').write_text(build.dumps(manifest)) + (self.source / 'resources').mkdir() + (self.source / 'resources/icon.svg').write_text('') + header = bytearray(64) + header[:6] = b'\x7fELF\x02\x01' + struct.pack_into('