diff --git a/.editorconfig b/.editorconfig index 82a15b8e8..f982778b0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -23,3 +23,19 @@ indent_size = 2 [**.nix] indent_style = space indent_size = 2 + +# shfmt reads these natively. Match existing script style to keep +# the format pass minimal. +[*.sh] +indent_style = space +indent_size = 4 +switch_case_indent = true + +[demo/throttle/enable] +indent_style = space +indent_size = 4 +switch_case_indent = true + +# Skip vendored / build output trees when shfmt walks the repo. +[{node_modules,target,dist,.venv}/**] +ignore = true diff --git a/.github/justfile b/.github/justfile new file mode 100644 index 000000000..137021cfc --- /dev/null +++ b/.github/justfile @@ -0,0 +1,9 @@ +set fallback + +# Lint GitHub Actions workflows. Silently skipped if actionlint is not installed. +check: + @if command -v actionlint >/dev/null 2>&1; then actionlint; fi + +# CI variant: actionlint is required (provided by the nix devShell). +ci: + actionlint diff --git a/.github/scripts/release.sh b/.github/scripts/release.sh index 89b4bc45d..8028f7a10 100755 --- a/.github/scripts/release.sh +++ b/.github/scripts/release.sh @@ -21,7 +21,7 @@ parse_version() { if [[ "$ref" =~ ^${prefix}-v([0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?(\+[0-9A-Za-z.-]+)?)$ ]]; then local version="${BASH_REMATCH[1]}" - echo "version=${version}" >> "$GITHUB_OUTPUT" + echo "version=${version}" >>"$GITHUB_OUTPUT" echo "Parsed version: ${version}" else echo "Tag format not recognized: $ref (expected ${prefix}-v)" >&2 @@ -36,10 +36,10 @@ prev_tag() { local current_tag="${GITHUB_REF#refs/tags/}" local prev - prev=$(git tag --list "${prefix}-v*" --sort=v:refname \ - | awk -v cur="$current_tag" '$0 == cur { print prev; found=1; exit } { prev=$0 } END { if (!found) print "" }') + prev=$(git tag --list "${prefix}-v*" --sort=v:refname | + awk -v cur="$current_tag" '$0 == cur { print prev; found=1; exit } { prev=$0 } END { if (!found) print "" }') - echo "tag=${prev}" >> "$GITHUB_OUTPUT" + echo "tag=${prev}" >>"$GITHUB_OUTPUT" echo "Previous tag: ${prev:-none}" } @@ -52,7 +52,7 @@ create_release() { local title="${RELEASE_TITLE:?RELEASE_TITLE must be set}" local prev_tag="${RELEASE_PREV_TAG:-}" - if gh release view "$tag" > /dev/null 2>&1; then + if gh release view "$tag" >/dev/null 2>&1; then echo "Release exists, updating assets and metadata..." gh release upload "$tag" "$artifacts_dir"/* --clobber if [ -n "$prev_tag" ]; then @@ -80,8 +80,8 @@ create_release() { # Dispatch subcommands case "${1:-}" in parse-version) parse_version "$2" ;; - prev-tag) prev_tag "$2" ;; - create) create_release "$2" ;; + prev-tag) prev_tag "$2" ;; + create) create_release "$2" ;; *) echo "Usage: $0 {parse-version|prev-tag|create} " >&2 exit 1 diff --git a/.github/scripts/render-formula.sh b/.github/scripts/render-formula.sh index 6de11136e..e156144f7 100755 --- a/.github/scripts/render-formula.sh +++ b/.github/scripts/render-formula.sh @@ -27,21 +27,21 @@ OUTPUT="" while [[ $# -gt 0 ]]; do case $1 in - --template|--version|--release-dir|--crate|--output) + --template | --version | --release-dir | --crate | --output) if [[ $# -lt 2 ]]; then echo "Error: $1 requires a value" >&2 exit 1 fi case $1 in - --template) TEMPLATE="$2" ;; - --version) VERSION="$2" ;; + --template) TEMPLATE="$2" ;; + --version) VERSION="$2" ;; --release-dir) RELEASE_DIR="$2" ;; - --crate) CRATE="$2" ;; - --output) OUTPUT="$2" ;; + --crate) CRATE="$2" ;; + --output) OUTPUT="$2" ;; esac shift 2 ;; - -h|--help) + -h | --help) echo "Usage: $0 --template T --version V --release-dir D --crate C --output O" exit 0 ;; @@ -98,5 +98,5 @@ if grep -q '__[A-Z0-9_]\+__' <<<"$rendered"; then fi mkdir -p "$(dirname "$OUTPUT")" -printf '%s\n' "$rendered" > "$OUTPUT" +printf '%s\n' "$rendered" >"$OUTPUT" echo "Wrote: $OUTPUT" diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 9172241c6..2e85990f8 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -23,8 +23,8 @@ jobs: run: | ref=${GITHUB_REF#refs/tags/} if [[ "$ref" =~ ^([a-z-]+)-v([0-9.]+)$ ]]; then - echo "target=${BASH_REMATCH[1]}" >> $GITHUB_OUTPUT - echo "version=${BASH_REMATCH[2]}" >> $GITHUB_OUTPUT + echo "target=${BASH_REMATCH[1]}" >> "$GITHUB_OUTPUT" + echo "version=${BASH_REMATCH[2]}" >> "$GITHUB_OUTPUT" else echo "Tag format not recognized." >&2 exit 1 @@ -101,6 +101,7 @@ jobs: - name: Create manifest working-directory: /tmp/digests run: | + # shellcheck disable=SC2046 # intentional word-splitting: one arg per digest docker buildx imagetools create \ -t ${{ env.REGISTRY }}/${{ needs.parse.outputs.target }}:${{ needs.parse.outputs.version }} \ -t ${{ env.REGISTRY }}/${{ needs.parse.outputs.target }}:latest \ diff --git a/.github/workflows/moq-cli.yml b/.github/workflows/moq-cli.yml index 25a1e7e59..332befc74 100644 --- a/.github/workflows/moq-cli.yml +++ b/.github/workflows/moq-cli.yml @@ -115,6 +115,7 @@ jobs: run: | # nfpm doesn't expand ${VAR} in contents[].src or templating # there either, so render the env vars in ourselves. + # shellcheck disable=SC2016 # envsubst varlist must be single-quoted envsubst '$VERSION $ARCH $BINARY_PATH' \ < packaging/moq-cli/nfpm.yaml > /tmp/nfpm.yaml nfpm pkg --packager deb --config /tmp/nfpm.yaml --target dist/ @@ -126,6 +127,7 @@ jobs: ARCH: ${{ matrix.rpm-arch }} BINARY_PATH: target/${{ matrix.target }}/release/moq run: | + # shellcheck disable=SC2016 # envsubst varlist must be single-quoted envsubst '$VERSION $ARCH $BINARY_PATH' \ < packaging/moq-cli/nfpm.yaml > /tmp/nfpm.yaml nfpm pkg --packager rpm --config /tmp/nfpm.yaml --target dist/ diff --git a/.github/workflows/moq-gst.yml b/.github/workflows/moq-gst.yml index da9ec1ad0..5d4538fe7 100644 --- a/.github/workflows/moq-gst.yml +++ b/.github/workflows/moq-gst.yml @@ -119,6 +119,7 @@ jobs: PLUGIN_DIR: /usr/lib/${{ matrix.deb-multiarch }}/gstreamer-1.0 run: | mkdir -p dist + # shellcheck disable=SC2016 # envsubst varlist must be single-quoted envsubst '$VERSION $ARCH $PKG_NAME $PLUGIN_PATH $PLUGIN_DIR' \ < packaging/moq-gst/nfpm.yaml > /tmp/nfpm.yaml nfpm pkg --packager deb --config /tmp/nfpm.yaml --target dist/ @@ -133,6 +134,7 @@ jobs: PLUGIN_DIR: /usr/lib64/gstreamer-1.0 run: | mkdir -p dist + # shellcheck disable=SC2016 # envsubst varlist must be single-quoted envsubst '$VERSION $ARCH $PKG_NAME $PLUGIN_PATH $PLUGIN_DIR' \ < packaging/moq-gst/nfpm.yaml > /tmp/nfpm.yaml nfpm pkg --packager rpm --config /tmp/nfpm.yaml --target dist/ diff --git a/.github/workflows/moq-relay.yml b/.github/workflows/moq-relay.yml index 0a52d3ec5..7dbf35623 100644 --- a/.github/workflows/moq-relay.yml +++ b/.github/workflows/moq-relay.yml @@ -104,6 +104,7 @@ jobs: ARCH: ${{ matrix.deb-arch }} BINARY_PATH: target/${{ matrix.target }}/release/moq-relay run: | + # shellcheck disable=SC2016 # envsubst varlist must be single-quoted envsubst '$VERSION $ARCH $BINARY_PATH' \ < packaging/moq-relay/nfpm.yaml > /tmp/nfpm.yaml nfpm pkg --packager deb --config /tmp/nfpm.yaml --target dist/ @@ -115,6 +116,7 @@ jobs: ARCH: ${{ matrix.rpm-arch }} BINARY_PATH: target/${{ matrix.target }}/release/moq-relay run: | + # shellcheck disable=SC2016 # envsubst varlist must be single-quoted envsubst '$VERSION $ARCH $BINARY_PATH' \ < packaging/moq-relay/nfpm.yaml > /tmp/nfpm.yaml nfpm pkg --packager rpm --config /tmp/nfpm.yaml --target dist/ diff --git a/.github/workflows/moq-token-cli.yml b/.github/workflows/moq-token-cli.yml index a5d65ac51..c6f024082 100644 --- a/.github/workflows/moq-token-cli.yml +++ b/.github/workflows/moq-token-cli.yml @@ -101,6 +101,7 @@ jobs: ARCH: ${{ matrix.deb-arch }} BINARY_PATH: target/${{ matrix.target }}/release/moq-token-cli run: | + # shellcheck disable=SC2016 # envsubst varlist must be single-quoted envsubst '$VERSION $ARCH $BINARY_PATH' \ < packaging/moq-token-cli/nfpm.yaml > /tmp/nfpm.yaml nfpm pkg --packager deb --config /tmp/nfpm.yaml --target dist/ @@ -112,6 +113,7 @@ jobs: ARCH: ${{ matrix.rpm-arch }} BINARY_PATH: target/${{ matrix.target }}/release/moq-token-cli run: | + # shellcheck disable=SC2016 # envsubst varlist must be single-quoted envsubst '$VERSION $ARCH $BINARY_PATH' \ < packaging/moq-token-cli/nfpm.yaml > /tmp/nfpm.yaml nfpm pkg --packager rpm --config /tmp/nfpm.yaml --target dist/ diff --git a/.github/workflows/release-brew.yml b/.github/workflows/release-brew.yml index 052a51cf8..b7f0c5476 100644 --- a/.github/workflows/release-brew.yml +++ b/.github/workflows/release-brew.yml @@ -115,9 +115,11 @@ jobs: fi crate="${BASH_REMATCH[1]}" version="${BASH_REMATCH[2]}" - echo "crate=$crate" >> "$GITHUB_OUTPUT" - echo "version=$version" >> "$GITHUB_OUTPUT" - echo "tag=$TAG" >> "$GITHUB_OUTPUT" + { + echo "crate=$crate" + echo "version=$version" + echo "tag=$TAG" + } >> "$GITHUB_OUTPUT" echo "Crate: $crate" echo "Version: $version" diff --git a/.taplo.toml b/.taplo.toml new file mode 100644 index 000000000..2d69dedde --- /dev/null +++ b/.taplo.toml @@ -0,0 +1,9 @@ +# Skip vendored / build trees so `taplo format` doesn't recurse into them. +exclude = ["**/node_modules/**", "**/target/**", "**/dist/**", "**/.venv/**"] + +# Match the existing repo style (4-space indent on Cargo.toml, lines up +# to ~120 chars) so the initial format pass stays minimal. +[formatting] +indent_string = " " +column_width = 150 +align_comments = false diff --git a/demo/boy/justfile b/demo/boy/justfile index 7534310ec..b5387236a 100644 --- a/demo/boy/justfile +++ b/demo/boy/justfile @@ -2,84 +2,85 @@ set fallback # Run the GB demo: relay + emulator publisher + web viewer. default: - bun install + bun install - bun run concurrently --kill-others --prefix-colors auto \ - "just relay" \ - "just wait && just b2s" \ - "just wait && just dangan" \ - "just wait && just web" + bun run concurrently --kill-others --prefix-colors auto \ + "just relay" \ + "just wait && just b2s" \ + "just wait && just dangan" \ + "just wait && just web" # Download the given ROM from our R2 bucket. download rom: - #!/usr/bin/env bash - if [ ! -f "rom/{{rom}}" ]; then - mkdir -p rom - curl -fSL -o "rom/{{rom}}.tmp" "https://rom.moq.dev/{{rom}}" - mv "rom/{{rom}}.tmp" "rom/{{rom}}" - fi + #!/usr/bin/env bash + if [ ! -f "rom/{{ rom }}" ]; then + mkdir -p rom + curl -fSL -o "rom/{{ rom }}.tmp" "https://rom.moq.dev/{{ rom }}" + mv "rom/{{ rom }}.tmp" "rom/{{ rom }}" + fi # Big2Small (puzzle): https://mdsteele.itch.io/big2small b2s: - just download big2small.gb - just start rom/big2small.gb + just download big2small.gb + just start rom/big2small.gb # Dangan GB2 (bullet hell): https://snorpung.itch.io/dangan-gb2 dangan: - just download DanganGB2.gbc - just start rom/DanganGB2.gbc + just download DanganGB2.gbc + just start rom/DanganGB2.gbc # Opossum Country (horror): https://benjelter.itch.io/opossum-country opossum: - just download OpossumCountry.gbc - just start rom/OpossumCountry.gbc + just download OpossumCountry.gbc + just start rom/OpossumCountry.gbc # Swordbird Song - Iron Owl Tower songbird: - just download Swordbird.gb - just start rom/Swordbird.gb + just download Swordbird.gb + just start rom/Swordbird.gb # RunieStory runiestory: - just download RunieStory.gb - just start rom/RunieStory.gb + just download RunieStory.gb + just start rom/RunieStory.gb # GB Run (racing): https://biscuitlocker.itch.io/gb-run gb-run: - just download gb-run.gbc - just start rom/gb-run.gbc + just download gb-run.gbc + just start rom/gb-run.gbc # Start a Game Boy emulator publisher. start rom url='http://localhost:4443/anon': - cargo run --bin moq-boy -- --url "{{url}}" --rom "{{rom}}" --location localhost + cargo run --bin moq-boy -- --url "{{ url }}" --rom "{{ rom }}" --location localhost # Host the web server web url='http://localhost:4443/anon': - VITE_RELAY_URL="{{url}}" bun --bun vite --open + VITE_RELAY_URL="{{ url }}" bun --bun vite --open # --- rom.moq.dev (R2 hosting) --- # Deploy the rom.moq.dev worker deploy: - bun install - bun wrangler deploy + bun install + bun wrangler deploy # Upload a single ROM to R2 upload file: - bun install - bun wrangler r2 object put "rom-moq-dev/{{file}}" --file "rom/{{file}}" --remote + bun install + bun wrangler r2 object put "rom-moq-dev/{{ file }}" --file "rom/{{ file }}" --remote # Sync all ROMs to R2 sync dir="rom": - #!/usr/bin/env bash - set -euo pipefail - for file in "{{dir}}"/*; do - key=$(basename "$file") - echo "Uploading $key..." - bun wrangler r2 object put "rom-moq-dev/$key" --file "$file" --remote - done + #!/usr/bin/env bash + set -euo pipefail + for file in "{{ dir }}"/*; do + [ -e "$file" ] || continue + key=$(basename "$file") + echo "Uploading $key..." + bun wrangler r2 object put "rom-moq-dev/$key" --file "$file" --remote + done # List files in the R2 bucket list: - bun install - bun wrangler r2 object list "rom-moq-dev" + bun install + bun wrangler r2 object list "rom-moq-dev" diff --git a/demo/justfile b/demo/justfile index 5631af82e..c94cd57e2 100644 --- a/demo/justfile +++ b/demo/justfile @@ -8,23 +8,23 @@ mod web # Run the web demo (default). default: - bun install - bun run concurrently --kill-others --names srv,bbb,web --prefix-colors auto \ - "just relay" \ - "just wait && just pub bbb" \ - "just wait && just web" + bun install + bun run concurrently --kill-others --names srv,bbb,web --prefix-colors auto \ + "just relay" \ + "just wait && just pub bbb" \ + "just wait && just web" # Wait for the localhost relay to be ready by repeatedly requesting the cert [private] wait url="http://localhost:4443/certificate.sha256": - #!/usr/bin/env bash - set -euo pipefail - for i in $(seq 1 600); do - curl -sf "{{url}}" > /dev/null 2>&1 && exit 0 - sleep 1 - done - exit 1 + #!/usr/bin/env bash + set -euo pipefail + for i in $(seq 1 600); do + curl -sf "{{ url }}" > /dev/null 2>&1 && exit 0 + sleep 1 + done + exit 1 # Throttle UDP traffic for testing (macOS only, requires sudo). throttle: - throttle/enable + throttle/enable diff --git a/demo/pub/justfile b/demo/pub/justfile index 79bbd0b42..ccf74cac0 100644 --- a/demo/pub/justfile +++ b/demo/pub/justfile @@ -4,246 +4,248 @@ set fallback # Initialize the bucket (this is mine; make your own) init: - bun install - bun wrangler r2 bucket create video-moq-dev + bun install + bun wrangler r2 bucket create video-moq-dev # Deploy the worker deploy: - bun install - bun wrangler deploy + bun install + bun wrangler deploy # Upload a single file to R2. # NOTE: Videos should be fragmented to minimize latency when streaming. # Use ffmpeg to fragment before uploading: # ffmpeg -i input.mp4 -c copy -f mp4 \ # -movflags cmaf+separate_moof+delay_moov+skip_trailer+frag_every_frame \ -# output.mp4 +# output.mp4 upload file: - bun install - bun wrangler r2 object put "video-moq-dev/{{file}}" --file "media/{{file}}" --remote + bun install + bun wrangler r2 object put "video-moq-dev/{{ file }}" --file "media/{{ file }}" --remote # Sync all files in a local directory to R2 sync dir="media": - #!/usr/bin/env bash - set -euo pipefail - for file in "{{dir}}"/*; do - key=$(basename "$file") - echo "Uploading $key..." - bun wrangler r2 object put "video-moq-dev/$key" --file "$file" --remote - done + #!/usr/bin/env bash + set -euo pipefail + for file in "{{ dir }}"/*; do + [ -e "$file" ] || continue + key=$(basename "$file") + echo "Uploading $key..." + bun wrangler r2 object put "video-moq-dev/$key" --file "$file" --remote + done # List files in the bucket list: - bun install - bun wrangler r2 object list "video-moq-dev" + bun install + bun wrangler r2 object list "video-moq-dev" # --- Publishing --- # Publish Big Buck Bunny to a relay server. bbb url='http://localhost:4443/anon' *args: - just download bbb - just cmaf bbb "{{url}}" {{args}} + just download bbb + just cmaf bbb "{{ url }}" {{ args }} # Publish Tears of Steel to a relay server. tos url='http://localhost:4443/anon' *args: - just download tos - just cmaf tos "{{url}}" {{args}} + just download tos + just cmaf tos "{{ url }}" {{ args }} # Publish a video using ffmpeg to a relay server. cmaf name url='http://localhost:4443/anon' *args: - cargo build --bin moq-cli - just ffmpeg-cmaf "media/{{name}}.mp4" |\ - cargo run --bin moq-cli -- \ - {{args}} publish --url "{{url}}" --name "{{name}}.hang" fmp4 + cargo build --bin moq-cli + just ffmpeg-cmaf "media/{{ name }}.mp4" |\ + cargo run --bin moq-cli -- \ + {{ args }} publish --url "{{ url }}" --name "{{ name }}.hang" fmp4 # Publish a video via iroh. iroh name url prefix="": - just download "{{name}}" - cargo build --bin moq-cli - just ffmpeg-cmaf "media/{{name}}.mp4" |\ - cargo run --bin moq-cli -- \ - --iroh-enabled publish --url "{{url}}" --name "{{prefix}}{{name}}.hang" fmp4 + just download "{{ name }}" + cargo build --bin moq-cli + just ffmpeg-cmaf "media/{{ name }}.mp4" |\ + cargo run --bin moq-cli -- \ + --iroh-enabled publish --url "{{ url }}" --name "{{ prefix }}{{ name }}.hang" fmp4 # Generate and ingest an HLS stream from a video file. hls name relay="http://localhost:4443/anon": - #!/usr/bin/env bash - set -euo pipefail - - just download "{{name}}" - - INPUT="media/{{name}}.mp4" - OUT_DIR="media/{{name}}" - - rm -rf "$OUT_DIR" - mkdir -p "$OUT_DIR" - - echo ">>> Generating HLS stream to disk (1280x720 + 256x144)..." - - ffmpeg -hide_banner -loglevel warning -re -stream_loop -1 -i "$INPUT" \ - -filter_complex "\ - [0:v]split=2[v0][v1]; \ - [v0]scale=-2:720[v720]; \ - [v1]scale=-2:144[v144]" \ - -map "[v720]" -map "[v144]" -map 0:a:0 \ - -r 25 -preset veryfast -g 50 -keyint_min 50 -sc_threshold 0 \ - -c:v:0 libx264 -profile:v:0 high -level:v:0 4.1 -pix_fmt:v:0 yuv420p -tag:v:0 avc1 \ - -b:v:0 4M -maxrate:v:0 4.4M -bufsize:v:0 8M \ - -c:v:1 libx264 -profile:v:1 high -level:v:1 4.1 -pix_fmt:v:1 yuv420p -tag:v:1 avc1 \ - -b:v:1 300k -maxrate:v:1 330k -bufsize:v:1 600k \ - -c:a aac -b:a 128k \ - -f hls -hls_time 2 -hls_list_size 6 \ - -hls_flags independent_segments+delete_segments \ - -hls_segment_type fmp4 \ - -master_pl_name master.m3u8 \ - -var_stream_map "v:0,agroup:audio,name:720 v:1,agroup:audio,name:144 a:0,agroup:audio,name:audio" \ - -hls_segment_filename "$OUT_DIR/v%v/segment_%09d.m4s" \ - "$OUT_DIR/v%v/stream.m3u8" & - - FFMPEG_PID=$! - - echo ">>> Waiting for HLS playlist generation..." - for i in {1..30}; do - if [ -f "$OUT_DIR/master.m3u8" ]; then break; fi - sleep 0.5 - done - - if [ ! -f "$OUT_DIR/master.m3u8" ]; then - kill $FFMPEG_PID 2>/dev/null || true - echo "Error: master.m3u8 not generated in time" - exit 1 - fi - - echo ">>> Waiting for variant playlists..." - sleep 2 - for i in {1..20}; do - if [ -f "$OUT_DIR/v0/stream.m3u8" ] || [ -f "$OUT_DIR/v720/stream.m3u8" ] || [ -f "$OUT_DIR/v144/stream.m3u8" ] || [ -f "$OUT_DIR/vaudio/stream.m3u8" ]; then - break - fi - sleep 0.5 - done - - CLEANUP_CALLED=false - cleanup() { - if [ "$CLEANUP_CALLED" = "true" ]; then return; fi - CLEANUP_CALLED=true - echo "Shutting down..." - kill $FFMPEG_PID 2>/dev/null || true - sleep 0.5 - kill -9 $FFMPEG_PID 2>/dev/null || true - } - trap cleanup SIGINT SIGTERM EXIT - - echo ">>> Running with --passthrough flag" - cargo run --bin moq-cli -- publish --url "{{relay}}" --name "{{name}}.hang" hls --playlist "$OUT_DIR/master.m3u8" --passthrough - EXIT_CODE=$? - - cleanup - exit $EXIT_CODE + #!/usr/bin/env bash + set -euo pipefail + + just download "{{ name }}" + + INPUT="media/{{ name }}.mp4" + OUT_DIR="media/{{ name }}" + + rm -rf "$OUT_DIR" + mkdir -p "$OUT_DIR" + + echo ">>> Generating HLS stream to disk (1280x720 + 256x144)..." + + ffmpeg -hide_banner -loglevel warning -re -stream_loop -1 -i "$INPUT" \ + -filter_complex "\ + [0:v]split=2[v0][v1]; \ + [v0]scale=-2:720[v720]; \ + [v1]scale=-2:144[v144]" \ + -map "[v720]" -map "[v144]" -map 0:a:0 \ + -r 25 -preset veryfast -g 50 -keyint_min 50 -sc_threshold 0 \ + -c:v:0 libx264 -profile:v:0 high -level:v:0 4.1 -pix_fmt:v:0 yuv420p -tag:v:0 avc1 \ + -b:v:0 4M -maxrate:v:0 4.4M -bufsize:v:0 8M \ + -c:v:1 libx264 -profile:v:1 high -level:v:1 4.1 -pix_fmt:v:1 yuv420p -tag:v:1 avc1 \ + -b:v:1 300k -maxrate:v:1 330k -bufsize:v:1 600k \ + -c:a aac -b:a 128k \ + -f hls -hls_time 2 -hls_list_size 6 \ + -hls_flags independent_segments+delete_segments \ + -hls_segment_type fmp4 \ + -master_pl_name master.m3u8 \ + -var_stream_map "v:0,agroup:audio,name:720 v:1,agroup:audio,name:144 a:0,agroup:audio,name:audio" \ + -hls_segment_filename "$OUT_DIR/v%v/segment_%09d.m4s" \ + "$OUT_DIR/v%v/stream.m3u8" & + + FFMPEG_PID=$! + + echo ">>> Waiting for HLS playlist generation..." + for i in {1..30}; do + if [ -f "$OUT_DIR/master.m3u8" ]; then break; fi + sleep 0.5 + done + + if [ ! -f "$OUT_DIR/master.m3u8" ]; then + kill $FFMPEG_PID 2>/dev/null || true + echo "Error: master.m3u8 not generated in time" + exit 1 + fi + + echo ">>> Waiting for variant playlists..." + sleep 2 + for i in {1..20}; do + if [ -f "$OUT_DIR/v0/stream.m3u8" ] || [ -f "$OUT_DIR/v720/stream.m3u8" ] || [ -f "$OUT_DIR/v144/stream.m3u8" ] || [ -f "$OUT_DIR/vaudio/stream.m3u8" ]; then + break + fi + sleep 0.5 + done + + CLEANUP_CALLED=false + cleanup() { + if [ "$CLEANUP_CALLED" = "true" ]; then return; fi + CLEANUP_CALLED=true + echo "Shutting down..." + kill $FFMPEG_PID 2>/dev/null || true + sleep 0.5 + kill -9 $FFMPEG_PID 2>/dev/null || true + } + trap cleanup SIGINT SIGTERM EXIT + + echo ">>> Running with --passthrough flag" + cargo run --bin moq-cli -- publish --url "{{ relay }}" --name "{{ name }}.hang" hls --playlist "$OUT_DIR/master.m3u8" --passthrough + EXIT_CODE=$? + + cleanup + exit $EXIT_CODE # Publish a video using H.264 Annex B format. h264 name url="http://localhost:4443/anon" *args: - just download "{{name}}" - cargo build --bin moq-cli + just download "{{ name }}" + cargo build --bin moq-cli - ffmpeg -hide_banner -v quiet \ - -stream_loop -1 -re \ - -i "media/{{name}}.mp4" \ - -c:v copy -an \ - -bsf:v h264_mp4toannexb \ - -f h264 \ - - | cargo run --bin moq-cli -- publish --url "{{url}}" --name "{{name}}.hang" avc3 {{args}} + ffmpeg -hide_banner -v quiet \ + -stream_loop -1 -re \ + -i "media/{{ name }}.mp4" \ + -c:v copy -an \ + -bsf:v h264_mp4toannexb \ + -f h264 \ + - | cargo run --bin moq-cli -- publish --url "{{ url }}" --name "{{ name }}.hang" avc3 {{ args }} # Publish a video using ffmpeg directly from moq to the localhost. serve name *args: - just download "{{name}}" - cargo build --bin moq-cli - just ffmpeg-cmaf "media/{{name}}.mp4" |\ - cargo run --bin moq-cli -- \ - {{args}} serve --listen "[::]:4443" --tls-generate "localhost" \ - --name "{{name}}.hang" fmp4 + just download "{{ name }}" + cargo build --bin moq-cli + just ffmpeg-cmaf "media/{{ name }}.mp4" |\ + cargo run --bin moq-cli -- \ + {{ args }} serve --listen "[::]:4443" --tls-generate "localhost" \ + --name "{{ name }}.hang" fmp4 # Generate and serve an HLS stream from a video for testing. serve-hls name port="8000": - #!/usr/bin/env bash - set -euo pipefail - - just download "{{name}}" - - INPUT="media/{{name}}.mp4" - OUT_DIR="media/{{name}}" - - rm -rf "$OUT_DIR" - mkdir -p "$OUT_DIR" - - echo ">>> Starting HLS stream generation..." - echo ">>> Master playlist: http://localhost:{{port}}/master.m3u8" - - cleanup() { - echo "Shutting down..." - kill $(jobs -p) 2>/dev/null || true - exit 0 - } - trap cleanup SIGINT SIGTERM - - ffmpeg -loglevel warning -re -stream_loop -1 -i "$INPUT" \ - -map 0:v:0 -map 0:v:0 -map 0:a:0 \ - -r 25 -preset veryfast -g 50 -keyint_min 50 -sc_threshold 0 \ - -c:v:0 libx264 -profile:v:0 high -level:v:0 4.1 -pix_fmt:v:0 yuv420p -tag:v:0 avc1 -bsf:v:0 dump_extra -b:v:0 4M -vf:0 "scale=1920:-2" \ - -c:v:1 libx264 -profile:v:1 high -level:v:1 4.1 -pix_fmt:v:1 yuv420p -tag:v:1 avc1 -bsf:v:1 dump_extra -b:v:1 300k -vf:1 "scale=256:-2" \ - -c:a aac -b:a 128k \ - -f hls \ - -hls_time 2 -hls_list_size 12 \ - -hls_flags independent_segments+delete_segments \ - -hls_segment_type fmp4 \ - -master_pl_name master.m3u8 \ - -var_stream_map "v:0,agroup:audio v:1,agroup:audio a:0,agroup:audio" \ - -hls_segment_filename "$OUT_DIR/v%v/segment_%09d.m4s" \ - "$OUT_DIR/v%v/stream.m3u8" & - - sleep 2 - echo ">>> HTTP server: http://localhost:{{port}}/" - cd "$OUT_DIR" && python3 -m http.server {{port}} + #!/usr/bin/env bash + set -euo pipefail + + just download "{{ name }}" + + INPUT="media/{{ name }}.mp4" + OUT_DIR="media/{{ name }}" + + rm -rf "$OUT_DIR" + mkdir -p "$OUT_DIR" + + echo ">>> Starting HLS stream generation..." + echo ">>> Master playlist: http://localhost:{{ port }}/master.m3u8" + + cleanup() { + echo "Shutting down..." + kill $(jobs -p) 2>/dev/null || true + exit 0 + } + trap cleanup SIGINT SIGTERM + + ffmpeg -loglevel warning -re -stream_loop -1 -i "$INPUT" \ + -map 0:v:0 -map 0:v:0 -map 0:a:0 \ + -r 25 -preset veryfast -g 50 -keyint_min 50 -sc_threshold 0 \ + -c:v:0 libx264 -profile:v:0 high -level:v:0 4.1 -pix_fmt:v:0 yuv420p -tag:v:0 avc1 -bsf:v:0 dump_extra -b:v:0 4M -vf:0 "scale=1920:-2" \ + -c:v:1 libx264 -profile:v:1 high -level:v:1 4.1 -pix_fmt:v:1 yuv420p -tag:v:1 avc1 -bsf:v:1 dump_extra -b:v:1 300k -vf:1 "scale=256:-2" \ + -c:a aac -b:a 128k \ + -f hls \ + -hls_time 2 -hls_list_size 12 \ + -hls_flags independent_segments+delete_segments \ + -hls_segment_type fmp4 \ + -master_pl_name master.m3u8 \ + -var_stream_map "v:0,agroup:audio v:1,agroup:audio a:0,agroup:audio" \ + -hls_segment_filename "$OUT_DIR/v%v/segment_%09d.m4s" \ + "$OUT_DIR/v%v/stream.m3u8" & + + sleep 2 + echo ">>> HTTP server: http://localhost:{{ port }}/" + cd "$OUT_DIR" && python3 -m http.server {{ port }} # Publish the clock broadcast. clock action url="http://localhost:4443/anon" *args: - @if [ "{{action}}" != "publish" ] && [ "{{action}}" != "subscribe" ]; then \ - echo "Error: action must be 'publish' or 'subscribe', got '{{action}}'" >&2; \ - exit 1; \ - fi + @if [ "{{ action }}" != "publish" ] && [ "{{ action }}" != "subscribe" ]; then \ + echo "Error: action must be 'publish' or 'subscribe', got '{{ action }}'" >&2; \ + exit 1; \ + fi - cargo run -p moq-native --example clock -- --url "{{url}}" --broadcast "clock" {{args}} {{action}} + cargo run -p moq-native --example clock -- --url "{{ url }}" --broadcast "clock" {{ args }} {{ action }} # Connect tokio-console to the publisher (port 6681). console: - tokio-console http://127.0.0.1:6681 + tokio-console http://127.0.0.1:6681 # --- GStreamer --- # Publish a video using GStreamer to a relay server. gst name url='http://localhost:4443/anon' *args: - just download "{{name}}" - cargo build -p moq-gst + just download "{{ name }}" + cargo build -p moq-gst - GST_PLUGIN_PATH_1_0="${PWD}/../../target/debug${GST_PLUGIN_PATH_1_0:+:$GST_PLUGIN_PATH_1_0}" \ - gst-launch-1.0 -v -e multifilesrc location="media/{{name}}.mp4" loop=true ! parsebin name=parse \ - parse. ! queue ! identity sync=true ! mux.sink_0 \ - parse. ! queue ! identity sync=true ! mux.sink_1 \ - moqsink name=mux url="{{url}}" broadcast="{{name}}.hang" {{args}} + GST_PLUGIN_PATH_1_0="${PWD}/../../target/debug${GST_PLUGIN_PATH_1_0:+:$GST_PLUGIN_PATH_1_0}" \ + gst-launch-1.0 -v -e multifilesrc location="media/{{ name }}.mp4" loop=true ! parsebin name=parse \ + parse. ! queue ! identity sync=true ! mux.sink_0 \ + parse. ! queue ! identity sync=true ! mux.sink_1 \ + moqsink name=mux url="{{ url }}" broadcast="{{ name }}.hang" {{ args }} # --- Private helpers --- # Download a test video (already fragmented on vid.moq.dev). [private] download name: - @if [ ! -f "media/{{name}}.mp4" ]; then \ - curl -fsSL "https://vid.moq.dev/{{name}}.mp4" -o "media/{{name}}.mp4"; \ - fi + @if [ ! -f "media/{{ name }}.mp4" ]; then \ + mkdir -p media; \ + curl -fsSL "https://vid.moq.dev/{{ name }}.mp4" -o "media/{{ name }}.mp4"; \ + fi # Convert an h264 input file to CMAF (fmp4) format to stdout. [private] ffmpeg-cmaf input output='-' *args: - ffmpeg -hide_banner -v quiet \ - -stream_loop -1 -re \ - -i "{{input}}" \ - -c copy \ - -f mp4 -movflags cmaf+separate_moof+delay_moov+skip_trailer+frag_every_frame {{args}} {{output}} + ffmpeg -hide_banner -v quiet \ + -stream_loop -1 -re \ + -i "{{ input }}" \ + -c copy \ + -f mp4 -movflags cmaf+separate_moof+delay_moov+skip_trailer+frag_every_frame {{ args }} {{ output }} diff --git a/demo/relay/justfile b/demo/relay/justfile index 229a480aa..53a8783f4 100644 --- a/demo/relay/justfile +++ b/demo/relay/justfile @@ -1,4 +1,4 @@ -set fallback := true +set fallback # Run a localhost relay server without authentication. default: diff --git a/demo/sub/justfile b/demo/sub/justfile index efcae70a1..411e09ad4 100644 --- a/demo/sub/justfile +++ b/demo/sub/justfile @@ -2,8 +2,8 @@ set fallback # Subscribe to a broadcast using GStreamer and render to the screen. gst name url='http://localhost:4443/anon' *args: - cargo build -p moq-gst + cargo build -p moq-gst - GST_PLUGIN_PATH_1_0="${PWD}/../../target/debug${GST_PLUGIN_PATH_1_0:+:$GST_PLUGIN_PATH_1_0}" \ - gst-launch-1.0 -v -e moqsrc url="{{url}}" broadcast="{{name}}" {{args}} \ - ! decodebin3 ! videoconvert ! autovideosink + GST_PLUGIN_PATH_1_0="${PWD}/../../target/debug${GST_PLUGIN_PATH_1_0:+:$GST_PLUGIN_PATH_1_0}" \ + gst-launch-1.0 -v -e moqsrc url="{{ url }}" broadcast="{{ name }}" {{ args }} \ + ! decodebin3 ! videoconvert ! autovideosink diff --git a/demo/throttle/enable b/demo/throttle/enable index 844ceabbd..aa62d703b 100755 --- a/demo/throttle/enable +++ b/demo/throttle/enable @@ -79,7 +79,7 @@ sudo cp "$PF_CONF" "$PF_BACKUP" grep -v "set skip on lo0" "$PF_BACKUP" echo "dummynet-anchor \"$PF_ANCHOR\"" echo "anchor \"$PF_ANCHOR\"" -} | sudo tee "$PF_CONF" > /dev/null +} | sudo tee "$PF_CONF" >/dev/null # Configure dummynet pipe sudo dnctl pipe $PIPE_NUM config bw "$BANDWIDTH" delay "$DELAY" queue "$QUEUE" diff --git a/demo/web/justfile b/demo/web/justfile index 275dc95ae..9f279a42a 100644 --- a/demo/web/justfile +++ b/demo/web/justfile @@ -2,8 +2,8 @@ set shell := ["bash", "-euo", "pipefail", "-c"] # Run the web server targeting the default relay. default: - just serve + just serve # Run the web server targeting the specified relay. serve url='http://localhost:4443/anon': - VITE_RELAY_URL="{{url}}" bun --bun vite --open + VITE_RELAY_URL="{{ url }}" bun --bun vite --open diff --git a/flake.nix b/flake.nix index 6abf1da60..48558ee21 100644 --- a/flake.nix +++ b/flake.nix @@ -62,28 +62,30 @@ ]; # Rust dependencies - rustDeps = with pkgs; [ - rust-toolchain - just - git - cmake - pkg-config - glib - libressl - ffmpeg - curl - cargo-sort - cargo-shear - cargo-edit - cargo-sweep - cargo-semver-checks - cargo-deny - ] - ++ gstreamerDeps - ++ pkgs.lib.optionals (!pkgs.stdenv.isDarwin) [ - # Marked broken on Darwin in nixpkgs, but builds fine on Linux. - pkgs.release-plz - ]; + rustDeps = + with pkgs; + [ + rust-toolchain + just + git + cmake + pkg-config + glib + libressl + ffmpeg + curl + cargo-sort + cargo-shear + cargo-edit + cargo-sweep + cargo-semver-checks + cargo-deny + ] + ++ gstreamerDeps + ++ pkgs.lib.optionals (!pkgs.stdenv.isDarwin) [ + # Marked broken on Darwin in nixpkgs, but builds fine on Linux. + pkgs.release-plz + ]; # JavaScript dependencies jsDeps = with pkgs; [ @@ -120,13 +122,26 @@ # Tools needed to regenerate and sign the apt/rpm repositories. # Linux-only because apt and createrepo_c are marked broken on Darwin # in nixpkgs. The publish workflows only ever run on Linux runners. - publishDeps = with pkgs; lib.optionals (!stdenv.isDarwin) [ - apt - createrepo_c - rpm - rclone - gnupg - gzip + publishDeps = + with pkgs; + lib.optionals (!stdenv.isDarwin) [ + apt + createrepo_c + rpm + rclone + gnupg + gzip + ]; + + # Linters / formatters required by `just ci`; `just check` and + # `just fix` guard each tool with `command -v` so they skip + # silently when the binary isn't on $PATH. + lintDeps = with pkgs; [ + shellcheck + shfmt + actionlint + taplo + nixfmt-rfc-style ]; # Apply our overlay to get the package definitions @@ -163,7 +178,7 @@ }; devShells.default = pkgs.mkShell { - packages = rustDeps ++ jsDeps ++ pyDeps ++ cdnDeps ++ packagingDeps; + packages = rustDeps ++ jsDeps ++ pyDeps ++ cdnDeps ++ packagingDeps ++ lintDeps; # jemalloc's configure uses -O0 test builds, which conflict with # Nix's _FORTIFY_SOURCE hardening (requires -O). diff --git a/go/justfile b/go/justfile index 5e90877a1..9647d2829 100644 --- a/go/justfile +++ b/go/justfile @@ -6,28 +6,28 @@ set working-directory := '.' default: - just check + just check # Build moq-ffi for the host, run uniffi-bindgen-go, stage everything # into a tmp dir, and run `go build`/`go vet`/`go test` from there. # Skips cleanly if cargo, go, or uniffi-bindgen-go is missing. check: - bash scripts/check.sh + bash scripts/check.sh # Stage the in-tree go/ source + per-target moq-ffi libs + generated # bindings into a single Go module ready for publish. package *args: - bash scripts/package.sh {{ args }} + bash scripts/package.sh {{ args }} # Full Go CI: `check` builds moq-ffi, regenerates bindings, runs go # vet/build/test. Takes a newline-separated list of changed files; # skips if FILES is non-empty and none match the Go scope. Run # `just go ci` (no FILES) to force-run. ci FILES="": - #!/usr/bin/env bash - set -euo pipefail - if [[ -n "{{ FILES }}" ]] && ! echo "{{ FILES }}" | grep -qE '^(go/|rs/moq-ffi/)'; then - echo "go: no Go changes; skipping." - exit 0 - fi - just check + #!/usr/bin/env bash + set -euo pipefail + if [[ -n "{{ FILES }}" ]] && ! echo "{{ FILES }}" | grep -qE '^(go/|rs/moq-ffi/)'; then + echo "go: no Go changes; skipping." + exit 0 + fi + just check diff --git a/go/scripts/check.sh b/go/scripts/check.sh index 94d941192..0427bab7f 100755 --- a/go/scripts/check.sh +++ b/go/scripts/check.sh @@ -36,23 +36,41 @@ echo "go check: building moq-ffi for $HOST_TARGET..." cargo build --release --package moq-ffi \ --manifest-path "$WORKSPACE_DIR/Cargo.toml" -TARGET_BASE=$(cargo metadata --format-version 1 --manifest-path "$WORKSPACE_DIR/Cargo.toml" --no-deps \ - | sed -n 's/.*"target_directory":"\([^"]*\)".*/\1/p') +TARGET_BASE=$(cargo metadata --format-version 1 --manifest-path "$WORKSPACE_DIR/Cargo.toml" --no-deps | + sed -n 's/.*"target_directory":"\([^"]*\)".*/\1/p') case "$HOST_TARGET" in - *-apple-*) CDYLIB="$TARGET_BASE/release/libmoq_ffi.dylib"; STATICLIB="$TARGET_BASE/release/libmoq_ffi.a";; - *-windows-*) CDYLIB="$TARGET_BASE/release/moq_ffi.dll"; STATICLIB="$TARGET_BASE/release/moq_ffi.lib";; - *) CDYLIB="$TARGET_BASE/release/libmoq_ffi.so"; STATICLIB="$TARGET_BASE/release/libmoq_ffi.a";; + *-apple-*) + CDYLIB="$TARGET_BASE/release/libmoq_ffi.dylib" + STATICLIB="$TARGET_BASE/release/libmoq_ffi.a" + ;; + *-windows-*) + CDYLIB="$TARGET_BASE/release/moq_ffi.dll" + STATICLIB="$TARGET_BASE/release/moq_ffi.lib" + ;; + *) + CDYLIB="$TARGET_BASE/release/libmoq_ffi.so" + STATICLIB="$TARGET_BASE/release/libmoq_ffi.a" + ;; esac -[[ -f "$CDYLIB" ]] || { echo "go check: cdylib not found at $CDYLIB" >&2; exit 1; } -[[ -f "$STATICLIB" ]] || { echo "go check: staticlib not found at $STATICLIB" >&2; exit 1; } +[[ -f "$CDYLIB" ]] || { + echo "go check: cdylib not found at $CDYLIB" >&2 + exit 1 +} +[[ -f "$STATICLIB" ]] || { + echo "go check: staticlib not found at $STATICLIB" >&2 + exit 1 +} # Reject unsupported hosts up front; package.sh derives the cgo # subdir name from the cargo target via its own mapping. case "$HOST_TARGET" in - x86_64-unknown-linux-gnu|aarch64-unknown-linux-gnu|x86_64-apple-darwin|aarch64-apple-darwin|x86_64-pc-windows-msvc) ;; - *) echo "go check: unsupported host target $HOST_TARGET" >&2; exit 1;; + x86_64-unknown-linux-gnu | aarch64-unknown-linux-gnu | x86_64-apple-darwin | aarch64-apple-darwin | x86_64-pc-windows-msvc) ;; + *) + echo "go check: unsupported host target $HOST_TARGET" >&2 + exit 1 + ;; esac # Stage into the workspace's dist/ (gitignored at repo root). diff --git a/go/scripts/package.sh b/go/scripts/package.sh index 1863a37de..742f61314 100755 --- a/go/scripts/package.sh +++ b/go/scripts/package.sh @@ -31,22 +31,49 @@ ARCHIVE=true while [[ $# -gt 0 ]]; do case $1 in - --version) VERSION="$2"; shift 2;; - --lib-dir) LIB_DIR="$2"; shift 2;; - --bindings-dir) BINDINGS_DIR="$2"; shift 2;; - --output) OUTPUT_DIR="$2"; shift 2;; - --no-archive) ARCHIVE=false; shift;; - -h|--help) + --version) + VERSION="$2" + shift 2 + ;; + --lib-dir) + LIB_DIR="$2" + shift 2 + ;; + --bindings-dir) + BINDINGS_DIR="$2" + shift 2 + ;; + --output) + OUTPUT_DIR="$2" + shift 2 + ;; + --no-archive) + ARCHIVE=false + shift + ;; + -h | --help) grep '^#' "$0" | sed 's/^# \{0,1\}//' exit 0 ;; - *) echo "Unknown option: $1" >&2; exit 1;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; esac done -[[ -z "$VERSION" ]] && { echo "Error: --version is required" >&2; exit 1; } -[[ -z "$LIB_DIR" ]] && { echo "Error: --lib-dir is required" >&2; exit 1; } -[[ -z "$BINDINGS_DIR" ]] && { echo "Error: --bindings-dir is required" >&2; exit 1; } +[[ -z "$VERSION" ]] && { + echo "Error: --version is required" >&2 + exit 1 +} +[[ -z "$LIB_DIR" ]] && { + echo "Error: --lib-dir is required" >&2 + exit 1 +} +[[ -z "$BINDINGS_DIR" ]] && { + echo "Error: --bindings-dir is required" >&2 + exit 1 +} [[ -z "$OUTPUT_DIR" ]] && OUTPUT_DIR="dist" mkdir -p "$OUTPUT_DIR" @@ -70,13 +97,19 @@ done # Dual-license files lifted from the workspace root. for license in LICENSE-MIT LICENSE-APACHE; do - [[ -f "$WORKSPACE_DIR/$license" ]] || { echo "Error: missing $WORKSPACE_DIR/$license" >&2; exit 1; } + [[ -f "$WORKSPACE_DIR/$license" ]] || { + echo "Error: missing $WORKSPACE_DIR/$license" >&2 + exit 1 + } cp "$WORKSPACE_DIR/$license" "$PKG_STAGE/$license" done # --- 2. Generated uniffi-bindgen-go output --- GENERATED_GO="$BINDINGS_DIR/moq/moq.go" -[[ -f "$GENERATED_GO" ]] || { echo "Error: uniffi-bindgen-go output not found at $GENERATED_GO" >&2; exit 1; } +[[ -f "$GENERATED_GO" ]] || { + echo "Error: uniffi-bindgen-go output not found at $GENERATED_GO" >&2 + exit 1 +} cp "$GENERATED_GO" "$PKG_STAGE/moq/moq.go" # --- 3. Per-target static libraries --- @@ -115,7 +148,7 @@ fi # --- 4. Minimal consumer-facing README rewrite --- # The full developer README lives in the monorepo; the staged copy # (which ends up on moq-dev/moq-go) gets a thin orientation pointer. -cat > "$PKG_STAGE/README.md" <"$PKG_STAGE/README.md" <&2; exit 1;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; esac done @@ -48,7 +54,10 @@ MIRROR_TAG="v${BUILD_VERSION}" SOURCE_TAG="moq-ffi-v${BUILD_VERSION}" TARBALL="go-out/moq-ffi-${BUILD_VERSION}-go.tar.gz" -[[ -f "$TARBALL" ]] || { echo "Error: missing $TARBALL" >&2; exit 1; } +[[ -f "$TARBALL" ]] || { + echo "Error: missing $TARBALL" >&2 + exit 1 +} WORK=$(mktemp -d) trap 'rm -rf "$WORK"' EXIT @@ -70,7 +79,10 @@ fi # --- 3. Extract staged package --- tar -xzf "$TARBALL" -C "$WORK" STAGED="$WORK/moq-ffi-${BUILD_VERSION}-go" -[[ -d "$STAGED" ]] || { echo "Error: tarball did not contain $STAGED" >&2; exit 1; } +[[ -d "$STAGED" ]] || { + echo "Error: tarball did not contain $STAGED" >&2 + exit 1 +} # --- 4. Replace mirror tree with staged contents (preserving .git) --- rsync --archive --delete --exclude='.git' "$STAGED/" "$WORK/mirror/" diff --git a/infra/apt/justfile b/infra/apt/justfile index 8fbf8cfc1..bfec5d405 100644 --- a/infra/apt/justfile +++ b/infra/apt/justfile @@ -2,15 +2,15 @@ set fallback # Deploy the apt.moq.dev worker. deploy: - bun install - bun wrangler deploy + bun install + bun wrangler deploy # Regenerate apt repo metadata and upload to the apt-moq-dev R2 bucket. # Reads .deb files from $ARTIFACTS_DIR (defaults to ./artifacts). publish artifacts="artifacts": - ARTIFACTS_DIR="{{artifacts}}" ./publish.sh + ARTIFACTS_DIR="{{ artifacts }}" ./publish.sh # List objects currently in the apt-moq-dev bucket. list: - bun install - bun wrangler r2 object list "apt-moq-dev" + bun install + bun wrangler r2 object list "apt-moq-dev" diff --git a/infra/apt/publish.sh b/infra/apt/publish.sh index 7b908edc4..be08cb2de 100755 --- a/infra/apt/publish.sh +++ b/infra/apt/publish.sh @@ -70,12 +70,12 @@ for arch in "${ARCHES[@]}"; do out="$WORK/dists/$DIST/$COMPONENT/binary-${arch}" mkdir -p "$out" (cd "$WORK" && apt-ftparchive --arch "$arch" packages "pool/$COMPONENT") \ - > "$out/Packages" + >"$out/Packages" gzip -9kf "$out/Packages" done echo ">> Generate Release..." -cat > "$WORK/apt-ftparchive.conf" <"$WORK/apt-ftparchive.conf" < "$WORK/dists/$DIST/Release" + >"$WORK/dists/$DIST/Release" echo ">> Sign Release..." GNUPGHOME=$(mktemp -d) @@ -95,8 +95,8 @@ chmod 700 "$GNUPGHOME" echo "${SIGNING_KEY:?}" | gpg --batch --quiet --import # Fail loud if SIGNING_KEY ever holds more than one secret. Silently picking # the first one would produce signatures from the wrong key. -mapfile -t KEY_IDS < <(gpg --list-secret-keys --with-colons --keyid-format=long \ - | awk -F: '/^sec:/ { print $5 }') +mapfile -t KEY_IDS < <(gpg --list-secret-keys --with-colons --keyid-format=long | + awk -F: '/^sec:/ { print $5 }') if [[ ${#KEY_IDS[@]} -ne 1 ]]; then echo "ERROR: expected exactly one secret key in SIGNING_KEY, found ${#KEY_IDS[@]}." >&2 exit 1 diff --git a/infra/justfile b/infra/justfile index 1879d2ed8..5edcd8ec8 100644 --- a/infra/justfile +++ b/infra/justfile @@ -5,5 +5,5 @@ mod rpm # Deploy all infra workers. deploy: - just apt deploy - just rpm deploy + just apt deploy + just rpm deploy diff --git a/infra/rpm/justfile b/infra/rpm/justfile index db7dae0e0..2c6b1be9b 100644 --- a/infra/rpm/justfile +++ b/infra/rpm/justfile @@ -2,15 +2,15 @@ set fallback # Deploy the rpm.moq.dev worker. deploy: - bun install - bun wrangler deploy + bun install + bun wrangler deploy # Regenerate rpm repo metadata and upload to the rpm-moq-dev R2 bucket. # Reads .rpm files from $ARTIFACTS_DIR (defaults to ./artifacts). publish artifacts="artifacts": - ARTIFACTS_DIR="{{artifacts}}" ./publish.sh + ARTIFACTS_DIR="{{ artifacts }}" ./publish.sh # List objects currently in the rpm-moq-dev bucket. list: - bun install - bun wrangler r2 object list "rpm-moq-dev" + bun install + bun wrangler r2 object list "rpm-moq-dev" diff --git a/infra/rpm/publish.sh b/infra/rpm/publish.sh index 6555c71c1..c1004317c 100755 --- a/infra/rpm/publish.sh +++ b/infra/rpm/publish.sh @@ -79,8 +79,8 @@ chmod 700 "$GNUPGHOME" echo "${SIGNING_KEY:?}" | gpg --batch --quiet --import # Fail loud if SIGNING_KEY ever holds more than one secret. Silently picking # the first one would produce signatures from the wrong key. -mapfile -t KEY_IDS < <(gpg --list-secret-keys --with-colons --keyid-format=long \ - | awk -F: '/^sec:/ { print $5 }') +mapfile -t KEY_IDS < <(gpg --list-secret-keys --with-colons --keyid-format=long | + awk -F: '/^sec:/ { print $5 }') if [[ ${#KEY_IDS[@]} -ne 1 ]]; then echo "ERROR: expected exactly one secret key in SIGNING_KEY, found ${#KEY_IDS[@]}." >&2 exit 1 @@ -102,7 +102,7 @@ for arch in "${ARCHES[@]}"; do done echo ">> Write moq.repo template..." -cat > "$WORK/moq.repo" <"$WORK/moq.repo" < check`. install: - bun install - cargo install --locked cargo-shear cargo-sort cargo-upgrades cargo-edit cargo-sweep cargo-semver-checks release-plz + bun install + cargo install --locked cargo-shear cargo-sort cargo-upgrades cargo-edit cargo-sweep cargo-semver-checks release-plz # Fast inner-loop checks. Runs JS, Rust, and Markdown lints. +# Shell + workflow + TOML + Nix + justfile lints skip silently if their +# binaries aren't on $PATH; `nix develop` provides them, and `just ci` +# requires them. check *args: - just js check - just rs check {{ args }} - bun remark . --quiet --frail + just js check + just rs check {{ args }} + bun remark . --quiet --frail + @if command -v shellcheck >/dev/null 2>&1 && command -v shfmt >/dev/null 2>&1; then shfmt --diff $(shfmt -f .) && shellcheck $(shfmt -f .); fi + @if command -v taplo >/dev/null 2>&1; then RUST_LOG=error taplo format --check; fi + @if command -v nixfmt >/dev/null 2>&1; then nixfmt --check $(find . -name '*.nix' -not -path './node_modules/*' -not -path './target/*' -not -path './.venv/*'); fi + @for f in $(find . -name justfile -not -path './node_modules/*' -not -path './target/*' -not -path './.venv/*'); do just --fmt --unstable --check --justfile "$f"; done + just gh check # Run every per-language `ci` with the diff vs BASE; each greps for its # own scope and skips when nothing relevant changed. Pass BASE="" to # default to $GITHUB_BASE_REF (CI) or origin/main (local). ci BASE="": - #!/usr/bin/env bash - set -euo pipefail - - # Resolve BASE: arg > $GITHUB_BASE_REF > origin/main. - if [[ -n "{{ BASE }}" ]]; then - base="{{ BASE }}" - elif [[ -n "${GITHUB_BASE_REF:-}" ]]; then - base="origin/${GITHUB_BASE_REF}" - else - base="origin/main" - fi - - # One git diff for the whole run; pass the file list to each per-lang. - merge_base=$(git merge-base "$base" HEAD) || { - echo "error: cannot resolve merge-base against $base (is full history fetched?)" >&2 - exit 1 - } - files=$(git diff --name-only "$merge_base") - - # Skip per-lang dispatch when nothing changed (empty FILES means - # "force-run" to per-lang, which is the wrong semantic here). - if [[ -n "$files" ]]; then - just js ci "$files" - just rs ci "$files" - just py ci "$files" - just kt ci "$files" - just swift ci "$files" - just go ci "$files" - fi - - # Cheap; always run. `bun install` is needed for remark-cli, since - # `just js ci` (where bun deps would otherwise install) is skipped - # when the diff has no JS-scoped files. - nix flake check - bun install --frozen-lockfile - bun remark . --quiet --frail + #!/usr/bin/env bash + set -euo pipefail + + # Resolve BASE: arg > $GITHUB_BASE_REF > origin/main. + if [[ -n "{{ BASE }}" ]]; then + base="{{ BASE }}" + elif [[ -n "${GITHUB_BASE_REF:-}" ]]; then + base="origin/${GITHUB_BASE_REF}" + else + base="origin/main" + fi + + # One git diff for the whole run; pass the file list to each per-lang. + merge_base=$(git merge-base "$base" HEAD) || { + echo "error: cannot resolve merge-base against $base (is full history fetched?)" >&2 + exit 1 + } + files=$(git diff --name-only "$merge_base") + + # Skip per-lang dispatch when nothing changed (empty FILES means + # "force-run" to per-lang, which is the wrong semantic here). + if [[ -n "$files" ]]; then + just js ci "$files" + just rs ci "$files" + just py ci "$files" + just kt ci "$files" + just swift ci "$files" + just go ci "$files" + fi + + # Cheap; always run. `bun install` is needed for remark-cli, since + # `just js ci` (where bun deps would otherwise install) is skipped + # when the diff has no JS-scoped files. + nix flake check + bun install --frozen-lockfile + bun remark . --quiet --frail + shfmt --diff $(shfmt -f .) + shellcheck $(shfmt -f .) + RUST_LOG=error taplo format --check + nixfmt --check $(find . -name '*.nix' -not -path './node_modules/*' -not -path './target/*' -not -path './.venv/*') + for f in $(find . -name justfile -not -path './node_modules/*' -not -path './target/*' -not -path './.venv/*'); do just --fmt --unstable --check --justfile "$f"; done + just gh ci # Auto-fix linting/formatting issues across all languages. +# shfmt / taplo / nixfmt / just --fmt skipped silently if missing locally. fix: - just js fix - just rs fix - just py fix - bun remark . --quiet --output + just js fix + just rs fix + just py fix + bun remark . --quiet --output + @if command -v shfmt >/dev/null 2>&1; then shfmt --write $(shfmt -f .); fi + @if command -v taplo >/dev/null 2>&1; then RUST_LOG=error taplo format; fi + @if command -v nixfmt >/dev/null 2>&1; then nixfmt $(find . -name '*.nix' -not -path './node_modules/*' -not -path './target/*' -not -path './.venv/*'); fi + @for f in $(find . -name justfile -not -path './node_modules/*' -not -path './target/*' -not -path './.venv/*'); do just --fmt --unstable --justfile "$f"; done # Run unit tests for every language. test *args: - just js test - just rs test {{ args }} - if command -v uv &> /dev/null; then just py test; fi + just js test + just rs test {{ args }} + if command -v uv &> /dev/null; then just py test; fi # Build the packages. build: - just js build - just rs build - if command -v uv &> /dev/null; then just py build; fi + just js build + just rs build + if command -v uv &> /dev/null; then just py build; fi # Upgrade any tooling update: - just js update - just rs update - nix flake update + just js update + just rs update + nix flake update # Serve the documentation locally. doc: - cd doc && bun run dev + cd doc && bun run dev diff --git a/kt/justfile b/kt/justfile index 6b6e826e7..9250aca97 100644 --- a/kt/justfile +++ b/kt/justfile @@ -7,28 +7,28 @@ set working-directory := '.' default: - just check + just check # Build moq-ffi for the host, regenerate uniffi bindings, run :moq:jvmTest. # Skips cleanly if cargo, java, or gradle is missing. check: - bash scripts/check.sh + bash scripts/check.sh # Assemble the KMP module from per-target moq-ffi binaries + bindings. # Used by .github/workflows/release-kt.yml; see kt/README.md for the # expected --lib-dir layout. package *args: - bash scripts/package.sh {{ args }} + bash scripts/package.sh {{ args }} # Full Kotlin CI: `check` already builds moq-ffi and runs gradle tests. # Takes a newline-separated list of changed files; skips if FILES is # non-empty and none match the Kotlin scope. Run `just kt ci` (no # FILES) to force-run. ci FILES="": - #!/usr/bin/env bash - set -euo pipefail - if [[ -n "{{ FILES }}" ]] && ! echo "{{ FILES }}" | grep -qE '^(kt/|rs/moq-ffi/)'; then - echo "kt: no Kotlin changes; skipping." - exit 0 - fi - just check + #!/usr/bin/env bash + set -euo pipefail + if [[ -n "{{ FILES }}" ]] && ! echo "{{ FILES }}" | grep -qE '^(kt/|rs/moq-ffi/)'; then + echo "kt: no Kotlin changes; skipping." + exit 0 + fi + just check diff --git a/kt/scripts/check.sh b/kt/scripts/check.sh index b8473ac29..d4f14c905 100755 --- a/kt/scripts/check.sh +++ b/kt/scripts/check.sh @@ -27,21 +27,36 @@ echo "kt check: building moq-ffi for $HOST_TARGET..." cargo build --release --package moq-ffi \ --manifest-path "$WORKSPACE_DIR/Cargo.toml" -TARGET_BASE=$(cargo metadata --format-version 1 --manifest-path "$WORKSPACE_DIR/Cargo.toml" --no-deps \ - | sed -n 's/.*"target_directory":"\([^"]*\)".*/\1/p') +TARGET_BASE=$(cargo metadata --format-version 1 --manifest-path "$WORKSPACE_DIR/Cargo.toml" --no-deps | + sed -n 's/.*"target_directory":"\([^"]*\)".*/\1/p') case "$HOST_TARGET" in - *-apple-*) CDYLIB="$TARGET_BASE/release/libmoq_ffi.dylib"; OS_TAG="darwin";; - *-windows-*) CDYLIB="$TARGET_BASE/release/moq_ffi.dll"; OS_TAG="win32";; - *) CDYLIB="$TARGET_BASE/release/libmoq_ffi.so"; OS_TAG="linux";; + *-apple-*) + CDYLIB="$TARGET_BASE/release/libmoq_ffi.dylib" + OS_TAG="darwin" + ;; + *-windows-*) + CDYLIB="$TARGET_BASE/release/moq_ffi.dll" + OS_TAG="win32" + ;; + *) + CDYLIB="$TARGET_BASE/release/libmoq_ffi.so" + OS_TAG="linux" + ;; esac case "$HOST_TARGET" in - aarch64-*) ARCH_TAG="aarch64";; - x86_64-*) ARCH_TAG="x86-64";; - *) echo "kt check: unsupported host arch in $HOST_TARGET" >&2; exit 1;; + aarch64-*) ARCH_TAG="aarch64" ;; + x86_64-*) ARCH_TAG="x86-64" ;; + *) + echo "kt check: unsupported host arch in $HOST_TARGET" >&2 + exit 1 + ;; esac -[[ -f "$CDYLIB" ]] || { echo "kt check: cdylib not found at $CDYLIB" >&2; exit 1; } +[[ -f "$CDYLIB" ]] || { + echo "kt check: cdylib not found at $CDYLIB" >&2 + exit 1 +} RES_DIR="$KT_DIR/moq/src/jvmMain/resources/${OS_TAG}-${ARCH_TAG}" mkdir -p "$RES_DIR" diff --git a/kt/scripts/package.sh b/kt/scripts/package.sh index 6d03ad6d3..6b5769b20 100755 --- a/kt/scripts/package.sh +++ b/kt/scripts/package.sh @@ -27,21 +27,45 @@ BINDINGS_DIR="" while [[ $# -gt 0 ]]; do case $1 in - --version) VERSION="$2"; shift 2;; - --lib-dir) LIB_DIR="$2"; shift 2;; - --output) OUTPUT_DIR="$2"; shift 2;; - --bindings-dir) BINDINGS_DIR="$2"; shift 2;; - -h|--help) + --version) + VERSION="$2" + shift 2 + ;; + --lib-dir) + LIB_DIR="$2" + shift 2 + ;; + --output) + OUTPUT_DIR="$2" + shift 2 + ;; + --bindings-dir) + BINDINGS_DIR="$2" + shift 2 + ;; + -h | --help) grep '^#' "$0" | sed 's/^# \{0,1\}//' exit 0 ;; - *) echo "Unknown option: $1" >&2; exit 1;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; esac done -[[ -z "$VERSION" ]] && { echo "Error: --version is required" >&2; exit 1; } -[[ -z "$LIB_DIR" ]] && { echo "Error: --lib-dir is required" >&2; exit 1; } -[[ -z "$BINDINGS_DIR" ]] && { echo "Error: --bindings-dir is required" >&2; exit 1; } +[[ -z "$VERSION" ]] && { + echo "Error: --version is required" >&2 + exit 1 +} +[[ -z "$LIB_DIR" ]] && { + echo "Error: --lib-dir is required" >&2 + exit 1 +} +[[ -z "$BINDINGS_DIR" ]] && { + echo "Error: --bindings-dir is required" >&2 + exit 1 +} [[ -z "$OUTPUT_DIR" ]] && OUTPUT_DIR="dist" mkdir -p "$OUTPUT_DIR" @@ -106,7 +130,10 @@ done # --- Uniffi-generated Kotlin source --- GENERATED_KT="$BINDINGS_DIR/uniffi/moq/moq.kt" -[[ -f "$GENERATED_KT" ]] || { echo "Error: uniffi-bindgen output not found at $GENERATED_KT" >&2; exit 1; } +[[ -f "$GENERATED_KT" ]] || { + echo "Error: uniffi-bindgen output not found at $GENERATED_KT" >&2 + exit 1 +} mkdir -p "$KT_DIR/moq/src/jvmAndAndroidMain/kotlin/uniffi/moq" cp "$GENERATED_KT" "$KT_DIR/moq/src/jvmAndAndroidMain/kotlin/uniffi/moq/moq.kt" @@ -120,6 +147,9 @@ if [[ "$HAVE_ANDROID_LIBS" == true ]]; then fi GRADLE_CMD="${GRADLE_CMD:-$(command -v gradle || true)}" -[[ -n "$GRADLE_CMD" ]] || { echo "Error: gradle not on PATH" >&2; exit 1; } +[[ -n "$GRADLE_CMD" ]] || { + echo "Error: gradle not on PATH" >&2 + exit 1 +} "$GRADLE_CMD" -p "$KT_DIR" "${GRADLE_ARGS[@]}" :moq:assemble :moq:publishToMavenLocal diff --git a/nix/modules/moq-relay.nix b/nix/modules/moq-relay.nix index 4bbbc71a9..89cfd9ac8 100644 --- a/nix/modules/moq-relay.nix +++ b/nix/modules/moq-relay.nix @@ -180,14 +180,17 @@ in # Generate cluster token for leaf nodes ${lib.optionalString (cfg.cluster.mode == "leaf" && cfg.auth.enable && cfg.cluster.tokenFile == null) - (let - keyPath = if cfg.auth.keyFile != null then cfg.auth.keyFile else "${cfg.stateDir}/root.jwk"; - in '' - ${pkgs.moq-token-cli}/bin/moq-token-cli --key "${keyPath}" sign \ - --subscribe "" --publish "" --cluster \ - > "${cfg.stateDir}/cluster.jwt" - chown ${cfg.user}:${cfg.group} "${cfg.stateDir}/cluster.jwt" - '') + ( + let + keyPath = if cfg.auth.keyFile != null then cfg.auth.keyFile else "${cfg.stateDir}/root.jwk"; + in + '' + ${pkgs.moq-token-cli}/bin/moq-token-cli --key "${keyPath}" sign \ + --subscribe "" --publish "" --cluster \ + > "${cfg.stateDir}/cluster.jwt" + chown ${cfg.user}:${cfg.group} "${cfg.stateDir}/cluster.jwt" + '' + ) } ''; diff --git a/nix/overlay.nix b/nix/overlay.nix index c83886550..056ef8525 100644 --- a/nix/overlay.nix +++ b/nix/overlay.nix @@ -48,7 +48,10 @@ in // { src = craneLib.cleanCargoSource ../.; cargoExtraArgs = "-p moq-boy --features jemalloc"; - nativeBuildInputs = with final; [ pkg-config clang ]; + nativeBuildInputs = with final; [ + pkg-config + clang + ]; buildInputs = with final; [ ffmpeg ]; LIBCLANG_PATH = "${final.libclang.lib}/lib"; # Enable frame pointers for profiling support (negligible overhead on x86_64). @@ -74,9 +77,7 @@ in src = final.lib.cleanSourceWith { src = ../.; name = "source"; - filter = - path: type: - (final.lib.hasSuffix ".pc.in" path) || (craneLib.filterCargoSources path type); + filter = path: type: (final.lib.hasSuffix ".pc.in" path) || (craneLib.filterCargoSources path type); }; cargoExtraArgs = "-p libmoq"; doCheck = false; diff --git a/py/justfile b/py/justfile index 974f0dd5d..c572b4efb 100644 --- a/py/justfile +++ b/py/justfile @@ -6,46 +6,46 @@ set working-directory := '.' default: - just check + just check # Lint + format + maturin source build + pyright. `--no-install-workspace` # installs the root dev group (ruff, maturin, pyright, pytest) without # trying to pip-build moq-rs; `maturin develop` then installs moq-rs # as an editable wheel with the rs/moq-ffi cdylib + uniffi bindings. check: - uv sync --no-install-workspace - uv run --no-sync ruff check . - uv run --no-sync ruff format --check . - cd moq-rs && uv run --no-sync maturin develop --uv - uv run --no-sync pyright + uv sync --no-install-workspace + uv run --no-sync ruff check . + uv run --no-sync ruff format --check . + cd moq-rs && uv run --no-sync maturin develop --uv + uv run --no-sync pyright fix: - uv sync --no-install-workspace - uv run --no-sync ruff check --fix . - uv run --no-sync ruff format . + uv sync --no-install-workspace + uv run --no-sync ruff check --fix . + uv run --no-sync ruff format . test: - uv sync --no-install-workspace - cd moq-rs && uv run --no-sync maturin develop --uv - uv run --no-sync pytest moq-rs/tests/ + uv sync --no-install-workspace + cd moq-rs && uv run --no-sync maturin develop --uv + uv run --no-sync pytest moq-rs/tests/ # Local dev build: produces an editable install of moq-rs (with the # moq-ffi cdylib + uniffi bindings) into the workspace venv. build: - uv sync --no-install-workspace - cd moq-rs && uv run --no-sync maturin develop --uv + uv sync --no-install-workspace + cd moq-rs && uv run --no-sync maturin develop --uv # Full Python CI: lint + tests + build. Takes a newline-separated list # of changed files; skips if FILES is non-empty and none match the # Python scope (which includes rs/moq-ffi because py bundles it via # maturin). Run `just py ci` (no FILES) to force-run everything. ci FILES="": - #!/usr/bin/env bash - set -euo pipefail - if [[ -n "{{ FILES }}" ]] && ! echo "{{ FILES }}" | grep -qE '^(py/|pyproject\.toml$|uv\.lock$|rs/moq-ffi/)'; then - echo "py: no Python changes; skipping." - exit 0 - fi - just check - just test - just build + #!/usr/bin/env bash + set -euo pipefail + if [[ -n "{{ FILES }}" ]] && ! echo "{{ FILES }}" | grep -qE '^(py/|pyproject\.toml$|uv\.lock$|rs/moq-ffi/)'; then + echo "py: no Python changes; skipping." + exit 0 + fi + just check + just test + just build diff --git a/rs/justfile b/rs/justfile index 1fb314a9d..cf4fb0a6f 100644 --- a/rs/justfile +++ b/rs/justfile @@ -10,16 +10,16 @@ set working-directory := '..' default: - just check + just check # Compile, lint, format-check, doc-check, and verify dependency hygiene. check *args: - cargo check --all-targets {{ args }} - cargo clippy --all-targets {{ args }} -- -D warnings - cargo fmt --all --check - RUSTDOCFLAGS="-D warnings" cargo doc --no-deps {{ args }} - cargo shear - cargo sort --workspace --check + cargo check --all-targets {{ args }} + cargo clippy --all-targets {{ args }} -- -D warnings + cargo fmt --all --check + RUSTDOCFLAGS="-D warnings" cargo doc --no-deps {{ args }} + cargo shear + cargo sort --workspace --check --no-format # Full Rust CI: check + feature edge cases + tests + build. Takes a # newline-separated list of changed files; skips if FILES is non-empty @@ -29,49 +29,49 @@ check *args: # `cargo deny` runs here (not in `check`) so the inner-loop stays fast # and devs aren't blocked by a fresh upstream advisory mid-edit. ci FILES="": - #!/usr/bin/env bash - set -euo pipefail - if [[ -n "{{ FILES }}" ]] && ! echo "{{ FILES }}" | grep -qE '^(rs/|Cargo\.toml$|Cargo\.lock$)'; then - echo "rs: no Rust changes; skipping." - exit 0 - fi - just check --workspace - cargo check --workspace --no-default-features - cargo check --workspace --all-features - cargo deny check --show-stats - just test --all-features - just build + #!/usr/bin/env bash + set -euo pipefail + if [[ -n "{{ FILES }}" ]] && ! echo "{{ FILES }}" | grep -qE '^(rs/|Cargo\.toml$|Cargo\.lock$)'; then + echo "rs: no Rust changes; skipping." + exit 0 + fi + just check --workspace + cargo check --workspace --no-default-features + cargo check --workspace --all-features + cargo deny check --show-stats + just test --all-features + just build # Auto-fix clippy/format/shear/sort, then sweep stale target/. fix: - cargo clippy --fix --allow-staged --allow-dirty --all-targets - cargo fmt --all - cargo shear --fix - cargo sort --workspace - if command -v cargo-sweep >/dev/null 2>&1; then cargo sweep --time 3; fi + cargo clippy --fix --allow-staged --allow-dirty --all-targets + cargo fmt --all + cargo shear --fix + cargo sort --workspace --no-format + if command -v cargo-sweep >/dev/null 2>&1; then cargo sweep --time 3; fi test *args: - cargo test --all-targets {{ args }} + cargo test --all-targets {{ args }} build: - cargo build + cargo build # Check semver compatibility against crates.io (default-members only). semver: - cargo semver-checks check-release + cargo semver-checks check-release # Update versions and changelogs via release-plz. bump: - release-plz update + release-plz update # Create release PRs and publish crates via release-plz. release: - release-plz release-pr --git-token "$GITHUB_TOKEN" - release-plz release --git-token "$GITHUB_TOKEN" + release-plz release-pr --git-token "$GITHUB_TOKEN" + release-plz release --git-token "$GITHUB_TOKEN" update: - cargo update - cargo upgrade --incompatible + cargo update + cargo upgrade --incompatible # Build a .deb or .rpm for one of the Rust binaries locally. nfpm comes # from the flake's dev shell (`nix develop`). For .rpm produced this way, @@ -80,36 +80,36 @@ update: # # Examples: # just rs package moq-relay deb -# just rs package moq-cli rpm +# just rs package moq-cli rpm package crate packager: - #!/usr/bin/env bash - set -euo pipefail - case "{{crate}}" in - moq-relay) bin=moq-relay ;; - moq-cli) bin=moq ;; - moq-token-cli) bin=moq-token-cli ;; - *) echo "Unknown crate: {{crate}} (use moq-relay, moq-cli, or moq-token-cli)" >&2; exit 1 ;; - esac - case "{{packager}}" in - deb) - if command -v dpkg >/dev/null 2>&1; then - arch=$(dpkg --print-architecture) - else - case "$(uname -m)" in - x86_64) arch=amd64 ;; - aarch64|arm64) arch=arm64 ;; - *) echo "Cannot infer deb arch from host $(uname -m)" >&2; exit 1 ;; - esac - fi - ;; - rpm) arch=$(uname -m) ;; - *) echo "Unknown packager: {{packager}} (use deb or rpm)" >&2; exit 1 ;; - esac - version=$(grep -m1 '^version' rs/{{crate}}/Cargo.toml | sed 's/.*"\(.*\)".*/\1/') - cargo build --release -p {{crate}} - mkdir -p dist - VERSION="$version" ARCH="$arch" BINARY_PATH="target/release/$bin" \ - nfpm pkg --packager {{packager}} \ - --config packaging/{{crate}}/nfpm.yaml \ - --target dist/ - ls -1 dist/ + #!/usr/bin/env bash + set -euo pipefail + case "{{ crate }}" in + moq-relay) bin=moq-relay ;; + moq-cli) bin=moq ;; + moq-token-cli) bin=moq-token-cli ;; + *) echo "Unknown crate: {{ crate }} (use moq-relay, moq-cli, or moq-token-cli)" >&2; exit 1 ;; + esac + case "{{ packager }}" in + deb) + if command -v dpkg >/dev/null 2>&1; then + arch=$(dpkg --print-architecture) + else + case "$(uname -m)" in + x86_64) arch=amd64 ;; + aarch64|arm64) arch=arm64 ;; + *) echo "Cannot infer deb arch from host $(uname -m)" >&2; exit 1 ;; + esac + fi + ;; + rpm) arch=$(uname -m) ;; + *) echo "Unknown packager: {{ packager }} (use deb or rpm)" >&2; exit 1 ;; + esac + version=$(grep -m1 '^version' rs/{{ crate }}/Cargo.toml | sed 's/.*"\(.*\)".*/\1/') + cargo build --release -p {{ crate }} + mkdir -p dist + VERSION="$version" ARCH="$arch" BINARY_PATH="target/release/$bin" \ + nfpm pkg --packager {{ packager }} \ + --config packaging/{{ crate }}/nfpm.yaml \ + --target dist/ + ls -1 dist/ diff --git a/rs/libmoq/build.sh b/rs/libmoq/build.sh index ef183943a..206e3fabf 100755 --- a/rs/libmoq/build.sh +++ b/rs/libmoq/build.sh @@ -22,10 +22,19 @@ OUTPUT_DIR="dist" while [[ $# -gt 0 ]]; do case $1 in - --target) TARGET="$2"; shift 2 ;; - --version) VERSION="$2"; shift 2 ;; - --output) OUTPUT_DIR="$2"; shift 2 ;; - -h|--help) + --target) + TARGET="$2" + shift 2 + ;; + --version) + VERSION="$2" + shift 2 + ;; + --output) + OUTPUT_DIR="$2" + shift 2 + ;; + -h | --help) echo "Usage: $0 [--target TARGET] [--version VERSION] [--output DIR]" exit 0 ;; @@ -67,10 +76,10 @@ if [[ "$TARGET" == *"-windows-"* ]]; then MAJOR_VERSION="${VERSION%%.*}" sed -e "s|@LIB_FILE@|${LIB_FILE}|g" \ -e "s|@VERSION@|${VERSION}|g" \ - "$SCRIPT_DIR/cmake/moq-config.cmake.in" > "$PACKAGE_DIR/lib/cmake/moq/moq-config.cmake" + "$SCRIPT_DIR/cmake/moq-config.cmake.in" >"$PACKAGE_DIR/lib/cmake/moq/moq-config.cmake" sed -e "s|@VERSION@|${VERSION}|g" \ -e "s|@MAJOR_VERSION@|${MAJOR_VERSION}|g" \ - "$SCRIPT_DIR/cmake/moq-config-version.cmake.in" > "$PACKAGE_DIR/lib/cmake/moq/moq-config-version.cmake" + "$SCRIPT_DIR/cmake/moq-config-version.cmake.in" >"$PACKAGE_DIR/lib/cmake/moq/moq-config-version.cmake" else echo "Building libmoq for $TARGET via nix..." BUILD_TMP="$(mktemp -d)" @@ -89,9 +98,9 @@ fi cd "$OUTPUT_DIR" if [[ "$TARGET" == *"-windows-"* ]]; then ARCHIVE="$NAME.zip" - if command -v 7z &> /dev/null; then + if command -v 7z &>/dev/null; then 7z a "$ARCHIVE" "$NAME" - elif command -v zip &> /dev/null; then + elif command -v zip &>/dev/null; then zip -r "$ARCHIVE" "$NAME" else echo "Error: Neither 7z nor zip found" >&2 diff --git a/rs/moq-ffi/build.sh b/rs/moq-ffi/build.sh index 2e7731fc7..762c508fa 100755 --- a/rs/moq-ffi/build.sh +++ b/rs/moq-ffi/build.sh @@ -15,9 +15,9 @@ RS_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" WORKSPACE_DIR="$(cd "$RS_DIR/.." && pwd)" # Resolve cargo target directory (respects CARGO_TARGET_DIR, .cargo/config, etc.) -TARGET_BASE_DIR=$(cargo metadata --format-version 1 --manifest-path "$WORKSPACE_DIR/Cargo.toml" --no-deps 2>/dev/null \ - | sed -n 's/.*"target_directory":"\([^"]*\)".*/\1/p' \ - || echo "$WORKSPACE_DIR/target") +TARGET_BASE_DIR=$(cargo metadata --format-version 1 --manifest-path "$WORKSPACE_DIR/Cargo.toml" --no-deps 2>/dev/null | + sed -n 's/.*"target_directory":"\([^"]*\)".*/\1/p' || + echo "$WORKSPACE_DIR/target") # Defaults TARGET="" @@ -51,7 +51,7 @@ while [[ $# -gt 0 ]]; do ARCHIVE=true shift ;; - -h|--help) + -h | --help) echo "Usage: $0 [--target TARGET] [--version VERSION] [--output DIR] [--bindings-only] [--archive]" exit 0 ;; @@ -249,9 +249,9 @@ if [[ "$ARCHIVE" == true ]]; then cd "$OUTPUT_DIR" if [[ "$TARGET" == *"-windows-"* ]]; then ARCHIVE_NAME="$NAME.zip" - if command -v 7z &> /dev/null; then + if command -v 7z &>/dev/null; then 7z a "$ARCHIVE_NAME" "$NAME" - elif command -v zip &> /dev/null; then + elif command -v zip &>/dev/null; then zip -r "$ARCHIVE_NAME" "$NAME" else echo "Error: Neither 7z nor zip found" >&2 @@ -273,9 +273,12 @@ if can_run_on_host "$TARGET"; then if [[ "$TARGET" == "universal-apple-darwin" ]]; then host_arch=$(uname -m) case "$host_arch" in - arm64|aarch64) cdylib=$(find_cdylib "aarch64-apple-darwin") ;; - x86_64) cdylib=$(find_cdylib "x86_64-apple-darwin") ;; - *) echo "Warning: unknown host arch $host_arch, skipping bindings"; cdylib="" ;; + arm64 | aarch64) cdylib=$(find_cdylib "aarch64-apple-darwin") ;; + x86_64) cdylib=$(find_cdylib "x86_64-apple-darwin") ;; + *) + echo "Warning: unknown host arch $host_arch, skipping bindings" + cdylib="" + ;; esac else cdylib=$(find_cdylib "$TARGET") diff --git a/rs/moq-gst/build.sh b/rs/moq-gst/build.sh index 4e747442a..247e75d02 100755 --- a/rs/moq-gst/build.sh +++ b/rs/moq-gst/build.sh @@ -19,10 +19,19 @@ OUTPUT_DIR="dist" while [[ $# -gt 0 ]]; do case $1 in - --target) TARGET="$2"; shift 2 ;; - --version) VERSION="$2"; shift 2 ;; - --output) OUTPUT_DIR="$2"; shift 2 ;; - -h|--help) + --target) + TARGET="$2" + shift 2 + ;; + --version) + VERSION="$2" + shift 2 + ;; + --output) + OUTPUT_DIR="$2" + shift 2 + ;; + -h | --help) echo "Usage: $0 [--target TARGET] [--version VERSION] [--output DIR]" exit 0 ;; diff --git a/rs/moq-gst/package.sh b/rs/moq-gst/package.sh index 3d5685b01..86a92524e 100755 --- a/rs/moq-gst/package.sh +++ b/rs/moq-gst/package.sh @@ -43,12 +43,32 @@ require_value() { while [[ $# -gt 0 ]]; do case $1 in - --packager) require_value "$@"; PACKAGER="$2"; shift 2 ;; - --version) require_value "$@"; VERSION="$2"; shift 2 ;; - --arch) require_value "$@"; PKG_ARCH="$2"; shift 2 ;; - --target) require_value "$@"; RUST_TARGET="$2"; shift 2 ;; - --output) require_value "$@"; OUTPUT_DIR="$2"; shift 2 ;; - -h|--help) + --packager) + require_value "$@" + PACKAGER="$2" + shift 2 + ;; + --version) + require_value "$@" + VERSION="$2" + shift 2 + ;; + --arch) + require_value "$@" + PKG_ARCH="$2" + shift 2 + ;; + --target) + require_value "$@" + RUST_TARGET="$2" + shift 2 + ;; + --output) + require_value "$@" + OUTPUT_DIR="$2" + shift 2 + ;; + -h | --help) sed -n '2,/^set -euo pipefail/p' "$0" | sed 's/^# //;s/^#//' | head -n -1 exit 0 ;; @@ -77,25 +97,29 @@ fi if [[ -z "$PKG_ARCH" ]]; then case "$RUST_TARGET" in x86_64-unknown-linux-*) - PKG_ARCH=$([[ "$PACKAGER" == "deb" ]] && echo "amd64" || echo "x86_64") ;; + PKG_ARCH=$([[ "$PACKAGER" == "deb" ]] && echo "amd64" || echo "x86_64") + ;; aarch64-unknown-linux-*) - PKG_ARCH=$([[ "$PACKAGER" == "deb" ]] && echo "arm64" || echo "aarch64") ;; + PKG_ARCH=$([[ "$PACKAGER" == "deb" ]] && echo "arm64" || echo "aarch64") + ;; *) echo "Cannot derive --arch from target $RUST_TARGET; please pass it explicitly." >&2 - exit 1 ;; + exit 1 + ;; esac echo "Derived pkg arch: $PKG_ARCH" fi # Plugin install directory varies by distro+arch. case "$PACKAGER:$PKG_ARCH" in - deb:amd64) PLUGIN_DIR="/usr/lib/x86_64-linux-gnu/gstreamer-1.0" ;; - deb:arm64) PLUGIN_DIR="/usr/lib/aarch64-linux-gnu/gstreamer-1.0" ;; - rpm:x86_64) PLUGIN_DIR="/usr/lib64/gstreamer-1.0" ;; - rpm:aarch64) PLUGIN_DIR="/usr/lib64/gstreamer-1.0" ;; + deb:amd64) PLUGIN_DIR="/usr/lib/x86_64-linux-gnu/gstreamer-1.0" ;; + deb:arm64) PLUGIN_DIR="/usr/lib/aarch64-linux-gnu/gstreamer-1.0" ;; + rpm:x86_64) PLUGIN_DIR="/usr/lib64/gstreamer-1.0" ;; + rpm:aarch64) PLUGIN_DIR="/usr/lib64/gstreamer-1.0" ;; *) echo "Unsupported --packager/--arch combination: $PACKAGER/$PKG_ARCH" >&2 - exit 1 ;; + exit 1 + ;; esac echo ">> Building moq-gst for $RUST_TARGET against system gstreamer..." diff --git a/rs/moq-native/Cargo.toml b/rs/moq-native/Cargo.toml index a3572ef73..c9b2c6ea1 100644 --- a/rs/moq-native/Cargo.toml +++ b/rs/moq-native/Cargo.toml @@ -17,13 +17,7 @@ doctest = false [features] default = ["quinn", "aws-lc-rs", "websocket"] -quinn = [ - "dep:quinn", - "dep:web-transport-quinn", - "dep:rcgen", - "dep:reqwest", - "dep:rustls-webpki", -] +quinn = ["dep:quinn", "dep:web-transport-quinn", "dep:rcgen", "dep:reqwest", "dep:rustls-webpki"] noq = ["dep:web-transport-noq", "dep:rcgen", "dep:reqwest", "dep:rustls-webpki"] quiche = ["dep:web-transport-quiche", "dep:rcgen"] aws-lc-rs = ["rustls/aws-lc-rs", "rcgen?/aws_lc_rs", "quinn?/rustls-aws-lc-rs"] diff --git a/rs/scripts/package-binary.sh b/rs/scripts/package-binary.sh index 3f93a4290..99836e2b1 100755 --- a/rs/scripts/package-binary.sh +++ b/rs/scripts/package-binary.sh @@ -20,20 +20,20 @@ OUTPUT_DIR="dist" while [[ $# -gt 0 ]]; do case $1 in - --crate|--target|--version|--output) + --crate | --target | --version | --output) if [[ $# -lt 2 ]]; then echo "Error: $1 requires a value" >&2 exit 1 fi case $1 in - --crate) CRATE="$2" ;; - --target) TARGET="$2" ;; + --crate) CRATE="$2" ;; + --target) TARGET="$2" ;; --version) VERSION="$2" ;; - --output) OUTPUT_DIR="$2" ;; + --output) OUTPUT_DIR="$2" ;; esac shift 2 ;; - -h|--help) + -h | --help) echo "Usage: $0 --crate CRATE [--target TARGET] [--version VERSION] [--output DIR]" exit 0 ;; diff --git a/swift/justfile b/swift/justfile index 285d22776..4a011add9 100644 --- a/swift/justfile +++ b/swift/justfile @@ -6,28 +6,28 @@ set working-directory := '.' default: - just check + just check # Build moq-ffi for the host, regenerate uniffi bindings, run swift test. # Skips cleanly on non-macOS hosts. check: - bash scripts/check.sh + bash scripts/check.sh # Assemble the XCFramework + Package.swift from per-target moq-ffi # binaries + bindings. Used by .github/workflows/release-swift.yml; see # swift/README.md for the expected --lib-dir layout. package *args: - bash scripts/package.sh {{ args }} + bash scripts/package.sh {{ args }} # Full Swift CI: `check` already builds moq-ffi and runs swift test. # Takes a newline-separated list of changed files; skips if FILES is # non-empty and none match the Swift scope. Run `just swift ci` (no # FILES) to force-run. ci FILES="": - #!/usr/bin/env bash - set -euo pipefail - if [[ -n "{{ FILES }}" ]] && ! echo "{{ FILES }}" | grep -qE '^(swift/|rs/moq-ffi/)'; then - echo "swift: no Swift changes; skipping." - exit 0 - fi - just check + #!/usr/bin/env bash + set -euo pipefail + if [[ -n "{{ FILES }}" ]] && ! echo "{{ FILES }}" | grep -qE '^(swift/|rs/moq-ffi/)'; then + echo "swift: no Swift changes; skipping." + exit 0 + fi + just check diff --git a/swift/scripts/check.sh b/swift/scripts/check.sh index 523c751d1..14aa1e040 100755 --- a/swift/scripts/check.sh +++ b/swift/scripts/check.sh @@ -44,12 +44,15 @@ echo "swift check: building moq-ffi for $HOST_TARGET..." cargo build --release --package moq-ffi \ --manifest-path "$WORKSPACE_DIR/Cargo.toml" -TARGET_BASE=$(cargo metadata --format-version 1 --manifest-path "$WORKSPACE_DIR/Cargo.toml" --no-deps \ - | sed -n 's/.*"target_directory":"\([^"]*\)".*/\1/p') +TARGET_BASE=$(cargo metadata --format-version 1 --manifest-path "$WORKSPACE_DIR/Cargo.toml" --no-deps | + sed -n 's/.*"target_directory":"\([^"]*\)".*/\1/p') CDYLIB="$TARGET_BASE/release/libmoq_ffi.dylib" STATIC="$TARGET_BASE/release/libmoq_ffi.a" -[[ -f "$CDYLIB" && -f "$STATIC" ]] || { echo "swift check: missing $CDYLIB or $STATIC" >&2; exit 1; } +[[ -f "$CDYLIB" && -f "$STATIC" ]] || { + echo "swift check: missing $CDYLIB or $STATIC" >&2 + exit 1 +} # Generate bindings. BINDGEN_OUT=$(mktemp -d) diff --git a/swift/scripts/package.sh b/swift/scripts/package.sh index f10df2733..6ad87cac1 100755 --- a/swift/scripts/package.sh +++ b/swift/scripts/package.sh @@ -32,27 +32,60 @@ RELEASE_URL_BASE="https://github.com/moq-dev/moq/releases/download" while [[ $# -gt 0 ]]; do case $1 in - --version) VERSION="$2"; shift 2;; - --lib-dir) LIB_DIR="$2"; shift 2;; - --output) OUTPUT_DIR="$2"; shift 2;; - --bindings-dir) BINDINGS_DIR="$2"; shift 2;; - --release-url) RELEASE_URL_BASE="$2"; shift 2;; - -h|--help) + --version) + VERSION="$2" + shift 2 + ;; + --lib-dir) + LIB_DIR="$2" + shift 2 + ;; + --output) + OUTPUT_DIR="$2" + shift 2 + ;; + --bindings-dir) + BINDINGS_DIR="$2" + shift 2 + ;; + --release-url) + RELEASE_URL_BASE="$2" + shift 2 + ;; + -h | --help) grep '^#' "$0" | sed 's/^# \{0,1\}//' exit 0 ;; - *) echo "Unknown option: $1" >&2; exit 1;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; esac done -[[ -z "$VERSION" ]] && { echo "Error: --version is required" >&2; exit 1; } -[[ -z "$LIB_DIR" ]] && { echo "Error: --lib-dir is required" >&2; exit 1; } +[[ -z "$VERSION" ]] && { + echo "Error: --version is required" >&2 + exit 1 +} +[[ -z "$LIB_DIR" ]] && { + echo "Error: --lib-dir is required" >&2 + exit 1 +} [[ -z "$OUTPUT_DIR" ]] && OUTPUT_DIR="dist" [[ -z "$BINDINGS_DIR" ]] && BINDINGS_DIR="$LIB_DIR/bindings" -[[ "$(uname)" == "Darwin" ]] || { echo "Error: package.sh requires macOS (xcodebuild)" >&2; exit 1; } -command -v xcodebuild >/dev/null || { echo "Error: xcodebuild not found" >&2; exit 1; } -command -v swift >/dev/null || { echo "Error: swift not found" >&2; exit 1; } +[[ "$(uname)" == "Darwin" ]] || { + echo "Error: package.sh requires macOS (xcodebuild)" >&2 + exit 1 +} +command -v xcodebuild >/dev/null || { + echo "Error: xcodebuild not found" >&2 + exit 1 +} +command -v swift >/dev/null || { + echo "Error: swift not found" >&2 + exit 1 +} mkdir -p "$OUTPUT_DIR" # Normalize to an absolute path: later steps (zip, swift package @@ -66,9 +99,18 @@ trap 'rm -rf "$STAGING"' EXIT # --- Headers (modulemap + .h) shared by all slices --- HEADERS_DIR="$STAGING/headers" mkdir -p "$HEADERS_DIR" -[[ -f "$BINDINGS_DIR/moqFFI.h" ]] || { echo "Error: missing $BINDINGS_DIR/moqFFI.h" >&2; exit 1; } -[[ -f "$BINDINGS_DIR/moqFFI.modulemap" ]] || { echo "Error: missing $BINDINGS_DIR/moqFFI.modulemap" >&2; exit 1; } -[[ -f "$BINDINGS_DIR/moq.swift" ]] || { echo "Error: missing $BINDINGS_DIR/moq.swift" >&2; exit 1; } +[[ -f "$BINDINGS_DIR/moqFFI.h" ]] || { + echo "Error: missing $BINDINGS_DIR/moqFFI.h" >&2 + exit 1 +} +[[ -f "$BINDINGS_DIR/moqFFI.modulemap" ]] || { + echo "Error: missing $BINDINGS_DIR/moqFFI.modulemap" >&2 + exit 1 +} +[[ -f "$BINDINGS_DIR/moq.swift" ]] || { + echo "Error: missing $BINDINGS_DIR/moq.swift" >&2 + exit 1 +} cp "$BINDINGS_DIR/moqFFI.h" "$HEADERS_DIR/" cp "$BINDINGS_DIR/moqFFI.modulemap" "$HEADERS_DIR/module.modulemap" @@ -80,7 +122,10 @@ lib_for() { ensure_lib() { local path path=$(lib_for "$1") - [[ -f "$path" ]] || { echo "Error: missing static lib for $1 at $path" >&2; exit 1; } + [[ -f "$path" ]] || { + echo "Error: missing static lib for $1 at $path" >&2 + exit 1 + } echo "$path" } @@ -123,13 +168,16 @@ cp "$BINDINGS_DIR/moq.swift" "$PKG_STAGE/Sources/MoqFFI/Generated.swift" # Dual-license files lifted from the workspace root so the mirror isn't # licenseless. Both files are required by the MIT OR Apache-2.0 grant. for license in LICENSE-MIT LICENSE-APACHE; do - [[ -f "$WORKSPACE_DIR/$license" ]] || { echo "Error: missing $WORKSPACE_DIR/$license" >&2; exit 1; } + [[ -f "$WORKSPACE_DIR/$license" ]] || { + echo "Error: missing $WORKSPACE_DIR/$license" >&2 + exit 1 + } cp "$WORKSPACE_DIR/$license" "$PKG_STAGE/$license" done # Minimal consumer-facing README. The full developer README lives in # the monorepo; this one just orients a visitor to moq-dev/moq-swift. -cat > "$PKG_STAGE/README.md" <"$PKG_STAGE/README.md" <&2; exit 1; } +[[ -f "$TEMPLATE" ]] || { + echo "Error: missing $TEMPLATE" >&2 + exit 1 +} URL="${RELEASE_URL_BASE}/moq-ffi-v${VERSION}/MoqFFI.xcframework.zip" # Token-based substitution: the template carries REPLACE_URL / REPLACE_VERSION # / REPLACE_CHECKSUM placeholders, so editing the upstream URL in the template @@ -161,7 +212,7 @@ URL="${RELEASE_URL_BASE}/moq-ffi-v${VERSION}/MoqFFI.xcframework.zip" sed -e "s|REPLACE_URL|${URL}|g" \ -e "s|REPLACE_VERSION|${VERSION}|g" \ -e "s|REPLACE_CHECKSUM|${CHECKSUM}|g" \ - "$TEMPLATE" > "$PKG_STAGE/Package.swift" + "$TEMPLATE" >"$PKG_STAGE/Package.swift" # Fail loudly if any placeholder survived (e.g. someone renamed a token # in the template without updating this script). Catching it here keeps @@ -176,7 +227,7 @@ fi # Swift toolchain. This runs even on PR dry-runs (where the live release # asset doesn't exist yet) and catches syntax / API breakage in the # template before it can reach the mirror. -(cd "$PKG_STAGE" && swift package dump-package > /dev/null) +(cd "$PKG_STAGE" && swift package dump-package >/dev/null) # --- Archive --- ARCHIVE="$OUTPUT_DIR/${PKG_NAME}.tar.gz" diff --git a/swift/scripts/publish.sh b/swift/scripts/publish.sh index 53d87c366..1ae3ad14b 100755 --- a/swift/scripts/publish.sh +++ b/swift/scripts/publish.sh @@ -25,17 +25,21 @@ set -euo pipefail # Expects the staged Swift package tarball under `swift-out/`, produced # by package.sh as `moq-ffi-${BUILD_VERSION}-swift.tar.gz`. -SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" - DRY_RUN=false while [[ $# -gt 0 ]]; do case $1 in - --dry-run) DRY_RUN=true; shift;; - -h|--help) + --dry-run) + DRY_RUN=true + shift + ;; + -h | --help) grep '^#' "$0" | sed 's/^# \{0,1\}//' exit 0 ;; - *) echo "Unknown option: $1" >&2; exit 1;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; esac done @@ -51,7 +55,10 @@ MIRROR_TAG="${BUILD_VERSION}" SOURCE_TAG="moq-ffi-v${BUILD_VERSION}" TARBALL="swift-out/moq-ffi-${BUILD_VERSION}-swift.tar.gz" -[[ -f "$TARBALL" ]] || { echo "Error: missing $TARBALL" >&2; exit 1; } +[[ -f "$TARBALL" ]] || { + echo "Error: missing $TARBALL" >&2 + exit 1 +} WORK=$(mktemp -d) trap 'rm -rf "$WORK"' EXIT @@ -76,7 +83,10 @@ fi # --- 3. Extract staged package --- tar -xzf "$TARBALL" -C "$WORK" STAGED="$WORK/moq-ffi-${BUILD_VERSION}-swift" -[[ -d "$STAGED" ]] || { echo "Error: tarball did not contain $STAGED" >&2; exit 1; } +[[ -d "$STAGED" ]] || { + echo "Error: tarball did not contain $STAGED" >&2 + exit 1 +} # --- 4. Replace mirror tree with staged contents (preserving .git) --- rsync --archive --delete --exclude='.git' "$STAGED/" "$WORK/mirror/" diff --git a/swift/scripts/verify.sh b/swift/scripts/verify.sh index 94664d2fa..2073ce704 100755 --- a/swift/scripts/verify.sh +++ b/swift/scripts/verify.sh @@ -23,13 +23,22 @@ TARBALL="" while [[ $# -gt 0 ]]; do case $1 in - --staged-dir) STAGED_DIR="$2"; shift 2;; - --tarball) TARBALL="$2"; shift 2;; - -h|--help) + --staged-dir) + STAGED_DIR="$2" + shift 2 + ;; + --tarball) + TARBALL="$2" + shift 2 + ;; + -h | --help) grep '^#' "$0" | sed 's/^# \{0,1\}//' exit 0 ;; - *) echo "Unknown option: $1" >&2; exit 1;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; esac done @@ -42,13 +51,19 @@ if [[ -z "$STAGED_DIR" && -z "$TARBALL" ]]; then exit 1 fi -command -v swift >/dev/null || { echo "Error: swift not found on PATH" >&2; exit 1; } +command -v swift >/dev/null || { + echo "Error: swift not found on PATH" >&2 + exit 1 +} WORK=$(mktemp -d) trap 'rm -rf "$WORK"' EXIT if [[ -n "$TARBALL" ]]; then - [[ -f "$TARBALL" ]] || { echo "Error: tarball not found: $TARBALL" >&2; exit 1; } + [[ -f "$TARBALL" ]] || { + echo "Error: tarball not found: $TARBALL" >&2 + exit 1 + } tar -xzf "$TARBALL" -C "$WORK" # The tarball wraps a single top-level moq-ffi-${VERSION}-swift dir. extracted=("$WORK"/moq-ffi-*-swift) @@ -62,7 +77,10 @@ fi # Resolve to absolute path; SPM resolves relative .package(path:) against # the consumer manifest, which lives under $WORK below. STAGED_DIR=$(cd "$STAGED_DIR" && pwd) -[[ -f "$STAGED_DIR/Package.swift" ]] || { echo "Error: $STAGED_DIR/Package.swift missing" >&2; exit 1; } +[[ -f "$STAGED_DIR/Package.swift" ]] || { + echo "Error: $STAGED_DIR/Package.swift missing" >&2 + exit 1 +} echo "verify: staged package at $STAGED_DIR" echo "verify: --- Package.swift ---" @@ -81,7 +99,7 @@ ln -s "$STAGED_DIR" "$PKG_LINK" SMOKE="$WORK/smoke" mkdir -p "$SMOKE/Sources/Smoke" -cat > "$SMOKE/Package.swift" <"$SMOKE/Package.swift" < "$SMOKE/Sources/Smoke/main.swift" <<'EOF' +cat >"$SMOKE/Sources/Smoke/main.swift" <<'EOF' import Moq // Verify that the binary target's symbols are linkable, not just resolvable. print("moq-swift verify ok")