# BEAM Apple VLCKit source candidate

This directory builds and audits self-built `MobileVLCKit.xcframework` for iOS
and `TVVLCKit.xcframework` for tvOS. The mobile plugin now resolves only the
reviewed iOS candidate staged by `stage_ios_consumer.py`; there is no public
CocoaPods fallback. The tvOS spike likewise accepts only a directory with
`TVVLCKit.xcframework` and `VERIFIED`.

Building from source is **not** legal approval. The module filter proves a
specific property of VLC plugin archives and the binary audit proves which
module entries survived the final link. Neither proves the effective license of
every contrib, satisfies LGPL replacement/relinking by itself, or resolves
codec patents, notices, trademarks, export rules, or App Store terms. Public
distribution remains blocked by the protected
`IOS_LIBVLC_DISTRIBUTION_APPROVED` gate and independent review of the exact
source, SBOM, notices, relinking mechanism, and final bytes.

libVLC is deliberately compiled only on the reviewed local Mac. GitHub Actions
never builds it. After review, `publish_candidate.sh` uploads the locally built
candidate exactly once to a protected draft release; app CI later downloads and
rehashes that immutable payload just as Android CI downloads the once-built
reviewed AAR.

## Pinned contract

`pins.env` pins:

- VLCKit 3.7.3 commit `319ed2c0724e3d4c4d34889a62fef1ae269491bc`;
- VLC commit `79128878ddb2c280bbb6c89c76a46b31a80ade1c`, the 47
  VLCKit patches, and their resulting tree;
- Xcode 26.6 (`17F113`), iOS/tvOS SDK 26.5, and 15.0 deployment
  minimums for all device and simulator slices;
- a fixed source epoch, module inventories, canonical physical build root,
  disk limits, and packaging revision.

The default source profile retains VLCKit 3.7.3's Meson 0.63.0. A pinned Meson
1.9.0 backport from VideoLAN VLCKit merge request !375 is available as the
`meson-1.9` profile, but it is not applied speculatively: the clean 3.7.3 build
on Xcode 26.6 completed the tool and device build with 0.63.0. Use the backport
only after reproducing the HarfBuzz/Ragel `_LIBCPP_ENABLE_ASSERTIONS` failure:

```sh
BEAM_VLCKIT_MESON_PROFILE=meson-1.9 \
  mobile/tool/vlckit-apple/build_ios_candidate.sh
```

Both profile trees and the Meson archive SHA-512 values are pinned.

The stable script did expose a separate Xcode 26 simulator probe error:
`pipe2` was detected from the macOS host and then compiled against an Apple SDK
where it is unavailable. The tracked build patch fixes this deterministically
with `ac_cv_func_pipe2=no`.

## Why the stock build is not acceptable

`--disable-gpl --disable-gnuv3` filters contrib selection; it does not filter
VLC's own statically linked plugins. The unmodified iOS arm64 install contained
241 unique module archives (flat and category copies): 233 exact LGPL markers
and eight exact GPL markers. Stock MobileVLCKit linked the GPL entries.

The first `beam.1` audit still missed a second licensing boundary:
`libzvbi_plugin.a` carried VLC's LGPL marker while its `libzvbi.a` contrib
dependency contributed objects whose exact sources are GPLv2. Exported
`vbi_decode_teletext_830*` symbols proved those objects survived the final
framework link. `beam.1` is quarantined. The `beam.2` patch disables zVBI in
both contrib and VLC configuration and explicitly enables VLC's separate
LGPL-marked `telx` module. The audits now reject `libzvbi.a`,
`libzvbi_plugin.a`, and any final `_vbi_` symbol independently of inventory
digests or marker classification.

The candidate patch calls `audit_static_modules.py` immediately before VLCKit
generates the built-in table and linker flags. For every iOS/tvOS device and
simulator archive it requires:

- a regular `lib*plugin.a`, with no symlinked roots, intermediate directories,
  or files;
- exactly one NUL-delimited VLC LGPL 2.1 marker or GPL v2 marker; mixed,
  unknown, malformed, or duplicate markers stop the build;
- exactly one defined `_vlc_entry__*` and its matching
  `_vlc_entry_license__*` symbol;
- correct single-architecture Mach-O members and a 15.0 Apple platform load
  command (legacy x86_64 simulator minimum commands are accepted only in the
  simulator x86_64 install prefix);
- byte-identical flat/category copies and identical semantic identities across
  device arm64 and simulator arm64/x86_64;
- the reviewed playback baseline in `baseline-modules.txt` and the exact
  per-product inventory/included/excluded digests from `pins.env`.

Only reviewed LGPL archive basenames are returned to the upstream linker.
Generated built-ins, the generated header, and each final framework
architecture must equal that approved entry set plus `vlc_entry__core` exactly.
The final audit also rejects every excluded/extra entry, GPL marker, wrong
platform/minimum/architecture, bitcode, code signature, non-system dynamic
dependency, mismatched dSYM UUID, missing `_vlc_static_modules`, or unexpected
XCFramework payload.

The Xcode archive link explicitly passes `-no_adhoc_codesign`. Xcode 26 signs
Apple Silicon simulator Mach-O outputs ad hoc by default even when project code
signing is disabled; suppressing that linker behavior keeps every distributed
framework slice unsigned so the consuming application owns final signing.

Core `libvlc.a`, `libvlccore.a`, and `vlc/libcompat.a` are audited separately.
For contrib archives, every object must have the exact slice CPU and every
declared Apple platform/minimum must match. Legitimate platformless assembly or
generated objects (including FFmpeg/libvpx members) are allowed only as a
named/count inventory in `contrib-platformless-members.tsv`; each archive must
still contain at least one member declaring the slice platform. Contrib
checksums and a defense-in-depth VLC marker scan are also recorded. Because
contribs do not use VLC module metadata, those checks do **not** establish
effective contrib licenses; a reviewed per-contrib license/SBOM allowlist is
still a distribution prerequisite.
Every downloaded tool/contrib source archive is independently checked against
VideoLAN's tracked `SHA512SUMS` catalogs.

## Reproducible path and disk policy

The build always uses the exact physical root:

```text
/private/tmp/beam-vlckit-apple-3.7.3-beam.2
```

VLC, FFmpeg, and other libraries embed configure strings. A user-specific
checkout cannot be made reproducible merely by asking the compiler to rewrite
arbitrary string literals, so the canonical root is part of the pinned input.
Compiler file/debug/macro prefix maps normalize source/debug paths to
`/usr/src/beam-vlckit`; Xcode DerivedData also stays under the canonical root.
Static inputs, final executables, and dSYMs reject `/Users/` and the repository
path. The canonical path is intentionally allowed because retained configure
strings are stable across builders.

A sibling operation lock next to the canonical root is held for the complete
prepare/build/audit operation. Nested scripts verify the inherited lock token;
separate workspaces therefore cannot replace the shared source tree during a
build. A stale lock after an interrupted process is fail-closed and must be
inspected before its exact lock directory is removed manually.

Preflight requires at least 40 GiB free. During every local upstream build,
`disk_guard.py` polls the canonical filesystem and terminates the complete
process group if free space drops below 15 GiB. Neither the two-slice iOS job nor
the four-slice Apple job runs on a GitHub-hosted or self-hosted Actions runner.

`preflight.sh` validates the exact Xcode build and only the SDKs/generic archive
destinations selected by its explicit `ios`, `tvos`, or `ios,tvos` argument:

- `generic/platform=iOS` with `iphoneos`;
- `generic/platform=iOS Simulator` with `iphonesimulator`;
- `generic/platform=tvOS` with `appletvos`;
- `generic/platform=tvOS Simulator` with `appletvsimulator`.

An SDK directory alone is insufficient. A simulator runtime is needed for
runtime tests, although a generic archive can sometimes resolve without one.

## First AppleTV inventory review

The AppleTV digest pins deliberately remain `PENDING_ACTUAL_APPLETV_AUDIT` in
the current tree. They must not be inferred from iOS or an official binary.
Capture all three real install inventories without linking a framework:

```sh
mobile/tool/vlckit-apple/capture_appletv_inventory.sh
```

The command builds tvOS device arm64 and simulator arm64/x86_64, writes the
complete archive/symbol/checksum evidence below `inventory-tvos.staging/audit`, and
stops at the pending review gate. It creates no `VERIFIED` marker. An
independent reviewer must compare the actual sets, baseline, excluded entries,
and source flags, then replace all three AppleTV pending values in `pins.env`.
The full candidate must be rebuilt from a clean canonical source after that
review.

## iOS-only calibration

After the generic-destination preflight is available, build and audit the
already reviewed iPhone inventory without attempting to package or publish:

```sh
mobile/tool/vlckit-apple/build_ios_calibration.sh
```

This performs a fresh prepare, validates the two iOS generic destinations, builds
only MobileVLCKit, audits its source/link inputs and final XCFramework, and
leaves evidence under `calibration-ios.staging`. It creates no package or
`VERIFIED` marker. Do not run it concurrently with an Xcode platform/runtime
installation.

## Local iOS candidate build

The shipping iPhone/iPad candidate is built locally without evaluating the
pending AppleTV inventory pins or tvOS SDK/destinations:

```sh
mobile/tool/vlckit-apple/build_ios_candidate.sh
```

This is the atomic `build_candidate.sh ios` path. It prepares exact source,
builds and audits the iOS device and simulator slices, packages corresponding
source and evidence, verifies every byte, and only then replaces `output-ios/`.
A failed run leaves any prior verified output untouched.

## Full build

After every inventory pin is reviewed:

```sh
mobile/tool/vlckit-apple/build_all_platforms.sh
```

The pipeline performs a fresh exact checkout, applies the tracked VLCKit build
patch and deterministic VLC patch series, runs preflight, builds iOS then tvOS
sequentially, audits all inputs/final binaries, packages deterministic assets,
and verifies every checksum. An existing verified output is replaced only
after the entire candidate passes.

The current iOS-only output remains ignored beneath:

```text
mobile/build/vlckit-apple-candidate/output-ios/
  VERIFIED
  SHA256SUMS
  BUILD-MANIFEST.txt
  SOURCE-MANIFEST.txt
  frameworks/
    MobileVLCKit.xcframework
  ios/
    VERIFIED
    MobileVLCKit.xcframework
    COPYING.txt
  audit/
  logs/
  artifacts/
    beam-mobilevlckit-ios-3.7.3-beam.2.tar.xz
    MobileVLCKit.podspec.json
    beam-vlckit-ios-corresponding-source-3.7.3-beam.2.tar.xz
    beam-vlckit-ios-audit-3.7.3-beam.2.tar.xz
```

The later complete iOS+tvOS build uses the parallel `output-apple/` topology
and adds `TVVLCKit.xcframework`, the `tvos/` consumer directory, and Apple-scope
source/audit assets. `BUILD-MANIFEST.txt` records `platforms=ios` or
`platforms=ios,tvos`; the verifier derives its exact allowed topology and
marker set from that value.

The corresponding-source archive contains both exact Git trees, BEAM patches
and tooling, downloaded source archives, and checksum manifests. Retain the
unstripped audit/dSYM material and source bundle for every shipped byte.

To reverify a preserved output:

```sh
mobile/tool/vlckit-apple/verify_candidate.sh \
  mobile/build/vlckit-apple-candidate/output-ios
```

For the tvOS spike, point `BEAM_TVOS_VLCKIT_CANDIDATE` at the complete
`output-apple` directory. The consumer reruns this package's full verifier and binds
the reviewed tvOS payload to the root manifests before either package manager
can link it. The separate distribution-approval environment gate remains
mandatory.

## One-time upload and CI verification

Actions must never compile this dependency. After verifying a local
`output-ios`, stage the exact bytes once in the private repository's draft
release from the Mac:

```sh
export BEAM_VLCKIT_UPLOAD_CONFIRM=beam-vlckit-ios-3.7.3-beam.2
mobile/tool/vlckit-apple/publish_candidate.sh \
  mobile/build/vlckit-apple-candidate/output-ios
```

The uploader first requires the repository to be private, reruns the full
candidate verifier, requires the exact confirmation tag, creates only a draft
release, refuses a published release, and refuses to append to a draft that
already has assets. It uploads the framework payload, podspec, corresponding
source, audit archive, and manifests; it never rebuilds libVLC and never
publishes the draft. This internal staging action is not public-distribution
approval and does not make the private source asset available to app
recipients.

The manual `verify-vlckit-apple-release.yml` workflow downloads those local-build
assets and runs only platform-neutral topology, checksum, archive-safety,
manifest, and podspec verification. It refuses a published release and retains
the checked inputs temporarily.

For a local app build, stage the already verified candidate without rebuilding:

```sh
python3 mobile/tool/vlckit-apple/stage_ios_consumer.py \
  --candidate mobile/build/vlckit-apple-candidate/output-ios
```

The stager reruns candidate checks, verifies every committed release hash,
extracts the framework into the ignored versioned consumer directory, reruns
the final Mach-O/module audit, and writes `VERIFIED` last. CocoaPods refuses to
resolve the app when that marker is absent.

After independent review, verify the protected draft and set the distribution
approval only when the separately reviewed source/relink delivery mechanism is
ready. This repository is private, so changing a release from draft to
published would not itself make corresponding source available to recipients.
The iOS app workflow requires the draft, reruns release topology/hash and macOS
binary audits, and stages the same local CocoaPods path. It never invokes any
VLCKit/libVLC build entry point. Do not use a short-lived Actions artifact, a CI
rebuild, or an unreviewed local build.

## Test the tooling

The unit suite exercises pins/patch assertions, exact marker boundaries,
duplicate symbol rejection, x86_64 legacy simulator provenance, symlink/path
traversal failures, platform/minimum checks and deterministic packaging:

```sh
PYTHONDONTWRITEBYTECODE=1 \
  python3 -m unittest mobile.tool.tests.test_vlckit_apple_tooling -v
```
