diff --git a/.cargo/config.toml b/.cargo/config.toml index eb70d03f..32451a6a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -8,6 +8,13 @@ rustflags = ["-D", "warnings"] # Treat warnings as errors during CI # Run tests with full backtrace on failure env = { RUST_BACKTRACE = "full", RUST_LOG = "debug" } +# The exact lint and test invocations CI runs, so a local pass means a CI pass. +# --workspace matters: the root is also a package, so a bare `cargo test` or +# `cargo clippy` covers srtla_send alone and skips the crates/ members. +[alias] +xlint = "clippy --workspace --all-targets --all-features -- -D warnings" +xtest = "test --workspace --all-features" + [profile.test] # Optimize tests for debugging debug = true diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 00000000..b23a435e --- /dev/null +++ b/.codespellrc @@ -0,0 +1,4 @@ +[codespell] +# Build output, the lockfile and the untracked reference checkouts listed in +# .gitignore are not ours to spell-check. +skip = ./.git,./target,./moblin,./bond-bunny-main,./.claude,./.impeccable,Cargo.lock diff --git a/.gitattributes b/.gitattributes index dfe07704..6c8590a1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,2 +1,3 @@ -# Auto detect text files and perform LF normalization -* text=auto +# LF everywhere, including Windows checkouts: the shell scripts under scripts/ +# run in CI and in WSL, and bash fails on CRLF line endings. +* text=auto eol=lf diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000..28290364 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,139 @@ +name: Bug report +description: srtla_send is not behaving the way it should +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Thanks for taking the time to report this. The debug log almost always contains + the answer, so reports with one attached get resolved much faster. + + - type: checkboxes + id: troubleshooting + attributes: + label: Before you file + description: Please work through these first. They resolve most reports. + options: + - label: I searched [existing issues](https://github.com/irlserver/srtla_send/issues?q=is%3Aissue) (including closed ones) and this is not already reported. + required: true + - label: I am on the [latest release](https://github.com/irlserver/srtla_send/releases) and the problem still happens there. + required: true + - label: I checked the README's [Troubleshooting](https://github.com/irlserver/srtla_send#troubleshooting) section. + required: true + - label: >- + Every uplink can reach the receiver on its own, and source routing is set up so + each IP in the IP list leaves through its own interface (see + [Assumptions and Prerequisites](https://github.com/irlserver/srtla_send#assumptions-and-prerequisites)). + required: false + - label: >- + If only some links carry traffic, I tried `--mode classic` to find out whether + the enhanced scheduler's quality scoring is the cause. + required: false + + - type: textarea + id: what-happened + attributes: + label: What happened + description: What you saw, and what you expected instead. + placeholder: | + After one modem drops and comes back, it never carries traffic again. + I expected the link to rejoin within a few seconds. + validations: + required: true + + - type: textarea + id: repro + attributes: + label: Steps to reproduce + description: What you did, in order. Include how long it took to show up. + placeholder: | + 1. Start srtla_send with two 4G modems + 2. Pull the SIM from one modem for 30 seconds + 3. Put it back; the link re-registers but stays idle + validations: + required: true + + - type: markdown + attributes: + value: | + ## The debug log + + **This is the single most useful thing you can attach.** Run srtla_send with + `RUST_LOG=debug`, reproduce the problem, and let it run for at least a minute + afterwards: + + ``` + RUST_LOG=debug srtla_send 6000 receiver.example.com 5000 ./uplinks.txt 2>&1 | tee srtla.log + ``` + + The log contains your receiver hostname and uplink IPs. Replace anything you would + rather not share before posting. If the log is long, attach it as a file instead of + pasting it. + + - type: textarea + id: log + attributes: + label: Debug log + description: The `RUST_LOG=debug` output around the time the problem happened, or an attached file. + render: text + validations: + required: true + + - type: input + id: version + attributes: + label: srtla_send version + description: The full output of `srtla_send -v`. + placeholder: "4.0.1 (main@df0b393) [srtla_send]" + validations: + required: true + + - type: dropdown + id: install + attributes: + label: How you installed it + options: + - .deb from a GitHub release + - Built from source + - Bundled with a device or distribution + validations: + required: true + + - type: input + id: platform + attributes: + label: Device and OS + placeholder: "Jetson Nano, Ubuntu 18.04 (arm64)" + validations: + required: true + + - type: textarea + id: setup + attributes: + label: Stream setup + description: > + The full srtla_send command line (with the receiver host removed if you prefer), any + config file, the uplinks and what they are (4G/5G modem, Starlink, Wi-Fi, Ethernet), + the receiver (srtla_rec version or service), and the encoder or SRT sender. + placeholder: | + srtla_send --control-socket /tmp/srtla.sock 6000 REDACTED 5000 /etc/srtla/ips + Uplinks: 2x 4G modem (USB), 1x Starlink via Ethernet + Receiver: srtla_rec from irlserver, latest + Encoder: belaUI, H.265 1080p30, 6 Mbps, SRT latency 2000 ms + validations: + required: true + + - type: textarea + id: stats + attributes: + label: Link stats + description: > + Optional but helpful: the output of `get_stats` over the control socket while the + problem is happening (see [Runtime Configuration](https://github.com/irlserver/srtla_send#runtime-configuration)). + render: json + + - type: textarea + id: extra + attributes: + label: Anything else + description: Graphs, metrics, or context that does not fit above. diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000..9445333d --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Documentation + url: https://github.com/irlserver/srtla_send#readme + about: Options, scheduling modes, runtime configuration and troubleshooting. Please check here before opening an issue. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000..be32eeb9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,34 @@ +name: Feature request +description: Suggest something srtla_send should do +labels: ["enhancement"] +body: + - type: checkboxes + id: checked + attributes: + label: Before you file + options: + - label: I searched [existing issues](https://github.com/irlserver/srtla_send/issues?q=is%3Aissue) (including closed ones) and this is not already suggested. + required: true + - label: I checked the README to confirm this is not already possible, including through the config file, the control socket or the command line options. + required: true + + - type: textarea + id: problem + attributes: + label: What problem does this solve + description: Describe the situation you hit, not only the feature. The underlying problem often has a better solution than the first one that comes to mind. + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: What you would like it to do + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: What you tried instead + description: Workarounds, other settings, other tools. diff --git a/.github/ISSUE_TEMPLATE/question.yml b/.github/ISSUE_TEMPLATE/question.yml new file mode 100644 index 00000000..c3642418 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.yml @@ -0,0 +1,37 @@ +name: Question or help +description: "\"How do I...\" or \"is this normal?\"" +labels: ["question"] +body: + - type: markdown + attributes: + value: | + Please read the [README](https://github.com/irlserver/srtla_send#readme) first. It covers + every option, the scheduling modes, runtime configuration and troubleshooting. + + - type: checkboxes + id: checked + attributes: + label: Before you ask + options: + - label: I searched [existing issues](https://github.com/irlserver/srtla_send/issues?q=is%3Aissue) (including closed ones). The same question may already have an answer. + required: true + - label: I read the relevant part of the README. + required: true + + - type: textarea + id: question + attributes: + label: Your question + description: What you are trying to do, and what is unclear or not behaving the way you expected. + validations: + required: true + + - type: textarea + id: setup + attributes: + label: Setup + description: If the question is about your stream, include the srtla_send version (`srtla_send -v`), device, uplinks and receiver. + placeholder: | + srtla_send 4.0.1 on a Jetson Nano, Ubuntu 18.04 + Uplinks: 2x 4G modem + Receiver: srtla_rec from irlserver diff --git a/.github/release-notes-header.md b/.github/release-notes-header.md new file mode 100644 index 00000000..4b551f3f --- /dev/null +++ b/.github/release-notes-header.md @@ -0,0 +1,20 @@ +## Installation + +Download the `.deb` for your device: `arm64` for ARM boards (Jetson, Orange Pi, Raspberry Pi and similar), `amd64` for x86 machines. Then install it: + +``` +sudo dpkg -i srtla_*_arm64.deb +``` + +The package installs `/usr/bin/srtla_send`. It runs on any Linux distribution with glibc 2.27 or newer, which includes Ubuntu 18.04 and Debian 10 and later. + +Verify the download against `sha256sums.txt`: + +``` +sha256sum --check --ignore-missing sha256sums.txt +``` + +For arguments and configuration, see the [README](https://github.com/irlserver/srtla_send#usage). + +--- + diff --git a/.github/workflows/build-debian.yml b/.github/workflows/build-debian.yml deleted file mode 100644 index 28ce4725..00000000 --- a/.github/workflows/build-debian.yml +++ /dev/null @@ -1,148 +0,0 @@ -name: Build and Upload Debian Package - -on: - push: - branches: - - main - - feat/improvements - release: - types: [released] - -jobs: - deb-arm64: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - # Install Rust and cross-compilation tools - - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: nightly - target: aarch64-unknown-linux-gnu - - # Install cross-compilation dependencies - - run: | - sudo apt update - sudo apt install -y gcc-aarch64-linux-gnu g++-aarch64-linux-gnu - sudo apt install -y libc6-dev-arm64-cross binutils-aarch64-linux-gnu - sudo apt install -y pkg-config - - # Verify cross-compiler setup - - name: Verify cross-compiler - run: | - aarch64-linux-gnu-gcc --version - rustup target list | grep aarch64-unknown-linux-gnu - - # Extract version from Cargo.toml - - name: Extract version - id: version - run: | - VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') - echo "cargo_version=$VERSION" >> $GITHUB_OUTPUT - echo "Extracted version: $VERSION" - - # Compile Rust project for ARM64 - - name: Build for ARM64 - env: - CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc - PKG_CONFIG_PATH: /usr/lib/aarch64-linux-gnu/pkgconfig - run: | - cargo build --release --target aarch64-unknown-linux-gnu --verbose - mkdir -p build/usr/bin - cp target/aarch64-unknown-linux-gnu/release/srtla_send build/usr/bin/ - - # Set up package structure - - name: Create package directories - run: | - mkdir -p build/DEBIAN - cat > build/DEBIAN/control << EOF - Package: srtla - Version: ${{ steps.version.outputs.cargo_version }} - Architecture: arm64 - Maintainer: Thomas Lekanger - Description: SRT transport proxy with link aggregation for connection bonding - Depends: libc6 - Homepage: https://irlserver.com - Section: unknown - EOF - - # Build the Debian package - - name: Build Debian package - run: | - dpkg-deb --build build - mv build.deb srtla_${{ steps.version.outputs.cargo_version }}_arm64.deb - - - uses: actions/upload-artifact@v4 - with: - name: srtla-arm64.deb - path: srtla_${{ steps.version.outputs.cargo_version }}_arm64.deb - - deb-amd64: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - # Install Rust toolchain - - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: nightly - target: x86_64-unknown-linux-gnu - - # Extract version from Cargo.toml - - name: Extract version - id: version - run: | - VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed 's/version = "\(.*\)"/\1/') - echo "cargo_version=$VERSION" >> $GITHUB_OUTPUT - echo "Extracted version: $VERSION" - - # Compile Rust project for AMD64 - - name: Build for AMD64 - run: | - cargo build --release --target x86_64-unknown-linux-gnu - mkdir -p build/usr/bin - cp target/x86_64-unknown-linux-gnu/release/srtla_send build/usr/bin/ - - # Set up package structure - - name: Create package directories - run: | - mkdir -p build/DEBIAN - cat > build/DEBIAN/control << EOF - Package: srtla - Version: ${{ steps.version.outputs.cargo_version }} - Architecture: amd64 - Maintainer: Thomas Lekanger - Description: SRT transport proxy with link aggregation for connection bonding - Depends: libc6 - Homepage: https://irlserver.com - Section: unknown - EOF - - # Build the Debian package - - name: Build Debian package - run: | - dpkg-deb --build build - mv build.deb srtla_${{ steps.version.outputs.cargo_version }}_amd64.deb - - - uses: actions/upload-artifact@v4 - with: - name: srtla-amd64.deb - path: srtla_${{ steps.version.outputs.cargo_version }}_amd64.deb - - release: - needs: [deb-arm64, deb-amd64] - runs-on: ubuntu-latest - if: github.event_name == 'release' - steps: - - uses: actions/checkout@v4 - - - uses: actions/download-artifact@v4 - with: - path: ./artifacts/ - - - uses: softprops/action-gh-release@v2 - with: - files: | - ./artifacts/srtla-arm64.deb/srtla_*_arm64.deb - ./artifacts/srtla-amd64.deb/srtla_*_amd64.deb - token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 00000000..27873c33 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,72 @@ +name: Build + +# Builds the release .debs on every push and pull request, so packaging breaks +# on the change that broke it and not on a tag. release.yml calls this same +# workflow, so a release ships exactly what CI built. +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + workflow_call: + +permissions: + contents: read + +env: + CARGO_TERM_COLOR: always + # Oldest glibc the shipped binaries may need. 2.27 is Ubuntu 18.04, the + # userland on L4T Jetson boards, the oldest devices the debs target. A plain + # cargo build links against the runner's glibc (2.39 on ubuntu-24.04); + # cargo-zigbuild links against this version's symbol set instead, and + # scripts/package-deb.sh refuses a binary that needs anything newer. + # Raising it drops support for older distributions. + GLIBC_FLOOR: "2.27" + ZIG_VERSION: "0.16.0" + CARGO_ZIGBUILD_VERSION: "0.23.4" + +jobs: + deb: + name: deb (${{ matrix.arch }}) + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - arch: amd64 + target: x86_64-unknown-linux-gnu + - arch: arm64 + target: aarch64-unknown-linux-gnu + steps: + - uses: actions/checkout@v7 + + # rust-toolchain.toml pins the channel; this installs it before the cache + # action reads the toolchain version for its key. + - name: Install the pinned Rust toolchain + run: | + rustup show active-toolchain || rustup toolchain install + rustup target add ${{ matrix.target }} + + - uses: Swatinem/rust-cache@v2 + with: + key: ${{ matrix.target }} + + - uses: mlugg/setup-zig@v2 + with: + version: ${{ env.ZIG_VERSION }} + + - uses: taiki-e/install-action@v2 + with: + tool: cargo-zigbuild@${{ env.CARGO_ZIGBUILD_VERSION }} + + - name: Build + run: cargo zigbuild --locked --profile release-lto --target ${{ matrix.target }}.${GLIBC_FLOOR} + + - name: Package + run: scripts/package-deb.sh target/${{ matrix.target }}/release-lto/srtla_send ${{ matrix.arch }} "${GLIBC_FLOOR}" dist + + - uses: actions/upload-artifact@v7 + with: + name: srtla-deb-${{ matrix.arch }} + path: dist/*.deb + if-no-files-found: error diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c433dcd..bbdf4b8c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,70 +2,105 @@ name: CI on: push: - branches: [main, master] + branches: [main] pull_request: - branches: [main, master] +permissions: + contents: read + +# A new push to a pull request makes the previous run's result irrelevant. +# Runs on main are kept so every merged commit has a result. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +env: + CARGO_TERM_COLOR: always + +# Every job builds with the toolchain pinned in rust-toolchain.toml unless it +# names another one with `cargo +`. The lint and test commands are +# the cargo aliases in .cargo/config.toml, so a local `cargo xlint` / `cargo +# xtest` gives the same answer as CI. jobs: - test: - name: Test + lint: + name: Lint runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: nightly - components: rustfmt, clippy + # Nightly only for rustfmt: rustfmt.toml uses unstable options. + - name: Install toolchains + run: | + rustup show active-toolchain || rustup toolchain install + rustup toolchain install nightly --profile minimal --component rustfmt - - name: Cache dependencies - uses: actions/cache@v4 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - target - key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-cargo- + - uses: Swatinem/rust-cache@v2 - - name: Check formatting - run: cargo fmt --all -- --check + - name: Format + run: cargo +nightly fmt --all -- --check - - name: Run clippy - run: cargo clippy -- -D warnings + - name: Clippy + run: cargo xlint - - name: Check compilation - run: cargo check + - name: Spell check + run: | + pipx install codespell==2.4.3 + codespell - - name: Check compilation without test features - run: cargo check --release + test: + name: Test (${{ matrix.os }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + steps: + - uses: actions/checkout@v7 - - name: Run unit tests - run: cargo test --lib --verbose + - name: Install the pinned Rust toolchain + run: rustup show active-toolchain || rustup toolchain install - - name: Run unit tests with test-internals (verify encapsulation) - run: cargo test --lib --features test-internals --verbose + - uses: Swatinem/rust-cache@v2 - - name: Run all tests - run: cargo test --features test-internals --verbose + # Default features first: the test-internals feature widens visibility, + # so this run proves the tests do not lean on it by accident. + - name: Test (default features) + run: cargo test --workspace --locked - - name: Run tests with all features - run: cargo test --all-features --verbose + - name: Test (all features) + run: cargo xtest --locked - - name: Install cargo-audit - run: cargo install cargo-audit + msrv: + name: MSRV + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 - - name: Run security audit - run: cargo audit + # Read from Cargo.toml so the declared rust-version is the one enforced. + - name: Install the MSRV toolchain + run: | + msrv="$(sed -nE 's/^rust-version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' Cargo.toml | head -n1)" + echo "MSRV=${msrv}" >>"${GITHUB_ENV}" + rustup toolchain install "${msrv}" --profile minimal - - name: Install cargo-deny - run: cargo install cargo-deny + - uses: Swatinem/rust-cache@v2 - - name: Run supply-chain checks (advisories + sources) - run: cargo deny check advisories sources + - name: Check + run: cargo +"${MSRV}" check --workspace --all-targets --all-features --locked + + beta: + name: Test (Rust beta) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Install beta + run: rustup toolchain install beta --profile minimal + + - uses: Swatinem/rust-cache@v2 + + - name: Test + run: cargo +beta test --workspace --all-features --locked # Miri lane over the only unsafe FFI in the tree: the recvmmsg batch-receive # path in src/connection/batch_recv.rs. Miri interprets the pure pointer logic @@ -78,70 +113,28 @@ jobs: name: Miri (batch_recv unsafe pointer logic) runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: nightly - components: miri + - name: Install nightly with miri + run: rustup toolchain install nightly --profile minimal --component miri,rust-src - name: Run batch_recv tests under miri - run: cargo miri test --lib batch_recv + run: cargo +nightly miri test --lib batch_recv - test-stable: - name: Test (Rust stable) + # cargo-deny's advisories check reads the same RustSec database as + # cargo-audit, so it replaces it, and it honors the ignores in deny.toml. + supply-chain: + name: Supply chain runs-on: ubuntu-latest steps: - - name: Checkout code - uses: actions/checkout@v4 + - uses: actions/checkout@v7 - - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: stable + - name: Install the pinned Rust toolchain + run: rustup show active-toolchain || rustup toolchain install - - name: Run tests - run: cargo test --features test-internals - - test-beta: - name: Test (Rust beta) - runs-on: ubuntu-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 + - uses: taiki-e/install-action@v2 with: - toolchain: beta - - - name: Run tests - run: cargo test + tool: cargo-deny - test-windows: - name: Test (Windows) - runs-on: windows-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 - - - name: Run tests - run: cargo test --features test-internals --verbose - - test-macos: - name: Test (macOS) - runs-on: macos-latest - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Install Rust toolchain - uses: actions-rust-lang/setup-rust-toolchain@v1 - - - name: Run tests - run: cargo test --verbose + - name: Advisories and sources + run: cargo deny check advisories sources diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 00000000..39642d13 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,76 @@ +name: Release + +# Pushing a vX.Y.Z tag builds the debs and creates a draft release. Nothing is +# public until a maintainer tests the artifacts and publishes the draft by +# hand. See RELEASING.md. +on: + push: + tags: ["v*"] + +permissions: + contents: read + +jobs: + check-version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + + - name: Verify tag matches the Cargo.toml version + run: | + tag_version="${GITHUB_REF_NAME#v}" + cargo_version="$(scripts/version.sh)" + echo "tag: ${tag_version} Cargo.toml: ${cargo_version}" + if [ "${tag_version}" != "${cargo_version}" ]; then + echo "::error::Tag ${GITHUB_REF_NAME} does not match version = \"${cargo_version}\" in Cargo.toml. Bump the version, commit, then move the tag." + exit 1 + fi + + build: + needs: check-version + uses: ./.github/workflows/build.yml + + release: + needs: build + runs-on: ubuntu-latest + permissions: + contents: write + steps: + # The changelog covers the commits since the previous tag, so this job + # needs full history and every tag. + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - uses: actions/download-artifact@v8 + with: + pattern: srtla-deb-* + merge-multiple: true + path: dist + + - name: Checksums + run: | + set -euo pipefail + (cd dist && sha256sum -- *.deb >sha256sums.txt) + ls -l dist + + - name: Build release notes + run: | + set -euo pipefail + { + cat .github/release-notes-header.md + echo "## What's changed" + echo + scripts/changelog.sh "${GITHUB_REF_NAME}" + } >notes.md + cat notes.md + + - name: Create draft release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "${GITHUB_REF_NAME}" dist/* \ + --draft \ + --verify-tag \ + --title "${GITHUB_REF_NAME}" \ + --notes-file notes.md diff --git a/AGENTS.md b/AGENTS.md index 6ec9f4aa..4515722c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,8 +28,8 @@ SRTLA Sender is a Rust implementation of the SRTLA bonding sender. SRTLA is a SR ### Tech Stack -- **Language**: Rust (Edition 2024, requires nightly toolchain) -- **Minimum Rust Version**: 1.87 +- **Language**: Rust (Edition 2024). `rust-toolchain.toml` pins the stable toolchain; nightly is used only for rustfmt +- **Minimum Rust Version**: the `rust-version` in `Cargo.toml`, checked by the CI MSRV job - **Async Runtime**: Tokio (multi-threaded runtime with macros, net, time, io-util, signal) - **CLI**: clap with derive features - **Logging**: tracing + tracing-subscriber with env-filter @@ -112,7 +112,7 @@ Tests are located in `src/tests/`: ### Formatting Configuration (rustfmt.toml) -The project uses **Rust nightly** with unstable rustfmt features: +rustfmt.toml uses unstable options, so formatting runs on nightly (`cargo +nightly fmt`): - Edition: 2024 - `unstable_features = true` @@ -148,9 +148,8 @@ The project uses **Rust nightly** with unstable rustfmt features: ### Important Constraints -- **Requires Rust nightly** due to unstable rustfmt features -- All formatting must pass `cargo fmt --all -- --check` -- All code must pass clippy with `-D warnings` (warnings as errors) +- Formatting needs nightly: all code must pass `cargo +nightly fmt --all -- --check`. The pinned stable toolchain installs without rustfmt, so a plain `cargo fmt` fails instead of formatting with stable rules +- All code must pass `cargo xlint` (clippy over the whole workspace with `-D warnings`) ## Robustness Behaviors @@ -245,57 +244,44 @@ cargo build --profile release-lto ### Testing -```bash -# Run all tests (requires nightly, with test-internals) -cargo test --features test-internals - -# Run with verbose output -cargo test --features test-internals --verbose +The root is also a package, so a bare `cargo test` covers srtla_send alone and skips the crates under `crates/`. Use `--workspace`, or the `xtest` alias that CI runs. -# Run all tests with all features -cargo test --all-features --verbose +```bash +# Run all tests in the workspace with all features (what CI runs) +cargo xtest -# Run only unit tests (library tests) -cargo test --lib --verbose +# Run the workspace tests with default features (verify encapsulation) +cargo test --workspace -# Run specific test +# Run a specific test cargo test test_connection_score - -# Run unit tests without test features (verify encapsulation) -cargo test --lib ``` ### Formatting ```bash -# Format code (requires nightly) -cargo fmt --all +# Format code (nightly) +cargo +nightly fmt --all # Check formatting without modifying -cargo fmt --all -- --check +cargo +nightly fmt --all -- --check ``` ### Linting ```bash -# Run clippy (warnings as errors) -cargo clippy -- -D warnings +# Run clippy over the workspace, warnings as errors (what CI runs) +cargo xlint -# Check compilation -cargo check - -# Check release compilation -cargo check --release +# Spell check (config in .codespellrc) +codespell ``` ### Security ```bash -# Install cargo-audit (first time only) -cargo install cargo-audit - -# Run security audit -cargo audit +# Advisories and crate sources (config in deny.toml) +cargo deny check advisories sources ``` ### Running @@ -318,41 +304,27 @@ When a coding task is completed, the following steps MUST be performed: ### 1. Format Code ```bash -cargo fmt --all -``` - -Verify it passes with: - -```bash -cargo fmt --all -- --check +cargo +nightly fmt --all ``` ### 2. Run Clippy ```bash -cargo clippy -- -D warnings +cargo xlint ``` All clippy warnings must be resolved (treated as errors). -### 3. Check Compilation - -```bash -cargo check -cargo check --release -``` - -### 4. Run Tests +### 3. Run Tests ```bash -# Run all tests with test-internals feature -cargo test --features test-internals --verbose +cargo xtest -# Optionally run without test features to verify encapsulation -cargo test --lib --verbose +# Optionally run with default features to verify encapsulation +cargo test --workspace ``` -### 5. Build Verification +### 4. Build Verification ```bash cargo build --release @@ -361,7 +333,7 @@ cargo build --release ## Style Guide - Write commit messages using [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/). -- Never bump the internal package version in `Cargo.toml`. This is handled automatically by the release process. +- Never bump the package version in `Cargo.toml`. The maintainer bumps it as part of a release (see RELEASING.md). - Rust files use LF line endings. ### Important Notes @@ -369,5 +341,5 @@ cargo build --release - **NEVER commit changes unless explicitly asked by the user** - All steps must pass before considering the task complete - Tests require the `test-internals` feature for full coverage -- The project requires Rust nightly toolchain +- Formatting requires the nightly toolchain; everything else uses the stable toolchain pinned in `rust-toolchain.toml` - Use `RUST_LOG=info` or `RUST_LOG=debug` for runtime debugging diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index c0cf206a..00000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,60 +0,0 @@ -# Changelog - -Changes on the `exp` branch since the `v3.0.0` release (https://github.com/irlserver/srtla_send/releases). - -This is an overview of the **current** state of `exp`, not a replay of every commit. Several ideas were prototyped and then removed before they reached this branch tip; those are listed under "Explored and removed" so the commit history makes sense. The version in `Cargo.toml` is still `3.0.0`, so this work is unreleased. - -## Scheduling and link selection - -The runtime still has exactly two scheduling modes: `classic` (capacity based, matches the original C behaviour) and `enhanced` (quality aware, the default). Enhanced selection gained an admission gate on top of its quality scoring: - -* **Weak-link admission gate.** A classifier (`sender/selection/classifier.rs`) marks a link weak when its RTT busts the chosen delay tier or its throughput share falls below an entering threshold, with enter/leave hysteresis. Enhanced selection excludes weak links from ranking when at least one healthy link can carry the packet, and falls back to the full pool when every link is weak (better to send on a weak link than drop the packet). -* **Congestion-aware skip.** Each connection carries a `cc_backing_off` flag set by the per-link CC controller. Enhanced selection treats a backing-off link as an extra weak signal and skips it under the same fallback rule. -* **Critical-packet priority.** During a keyframe (IDR / SPS / PPS) the scheduler routes packets to the highest-quality link. Two triggers feed this: a heuristic burst detector (`sender/keyframe.rs`, which declares a burst after 5 consecutive max-MTU 1316-byte packets), and an explicit critical window opened by an upstream encoder (see the priority sidecar below). - -## RTT estimation - -* **Kalman filter as the primary RTT estimator.** `RttTracker` now uses a 2-state Kalman filter (value, velocity) as its smooth RTT source, replacing the old smooth/fast EWMA pair. The filter tracks trends naturally, so its velocity term doubles as spike detection. A new `kalman.rs` module holds the filter and its RTT preset. -* **Dual-window minimum tracking and a sample filter.** RTT keeps a fast and a slow minimum window plus a small min-sample filter for stability, exposed through `LinkStats`. -* EWMA is retained only for the symmetric `rtt_avg_delta`. The asymmetric (fast-down, slow-up) EWMA variant exists in the tree but is `#[cfg(test)]` only, so it does not compile into the production binary. - -## Per-link congestion control - -* **`link_cc` controller** (`sender/selection/link_cc.rs`). A per-connection state machine (`CcState`: Bootstrap, Climbing, Holding, BackingOff, Drain) with a `ClimbMode` sub-state (Hai, FastRecovery, Normal) that produces a `target_bps` soft cap from age-bucketed RTT EWMA, RTT variance, a sliding-window loss permille, and observed bitrate. -* **Current wiring.** Only the `BackingOff` state influences selection today (via the `cc_backing_off` flag described above). The `target_bps` soft cap is computed and exported for telemetry but is not yet applied as a rate cap in the data path. - -## Transport and egress - -* **Pluggable uplink binder.** A new `UplinkBinder` trait makes egress steering injectable. `SourceIpBinder` keeps the existing source-IP bind (Linux source routing); a callback binder lets a library consumer steer the raw fd (for example Android `Network.bindSocket`) while keeping `IpAddr` as the uplink identity. The binder is threaded through sender startup, connection creation, and reconnect so the same binding is re-applied on reconnect. -* **Adaptive batch-send regimes.** `batch_send.rs` picks a batch threshold per connection from observed bitrate: LowActivity (under 500 kbps, threshold 4), Normal (500 kbps to 5 Mbps, threshold 16), HighLoad (above 5 Mbps, threshold 32). The 15ms flush interval is unchanged. The regime is recomputed once per housekeeping tick. - -## Configuration, control, and observability - -* **TOML config file.** A `--config` flag loads tunable constants from TOML (`toml_config.rs`), falling back to defaults on error, reloaded on SIGHUP. -* **Dynamic runtime config.** `DynamicConfig` replaces the old `DynamicToggles`, holding the scheduling mode and toggles behind atomics for thread-safe runtime changes over stdin and a control socket. -* **JSON-RPC control socket.** A Unix-socket control plane (`control.rs`, `control_socket.rs`, `--control-socket`) supports `set_mode` (classic or enhanced), `set_quality`, `get_status`, `get_stats`, and `subscribe` / `unsubscribe` to the `stats` and `priority.window` topics. Documented in `docs/CONTROL_PROTOCOL.md`. -* **Critical-packet priority sidecar.** A dedicated UDP socket (`priority.rs`, `--priority-bind`) takes a 5-byte datagram from an encoder to open a critical window of N milliseconds. Loopback UDP shares the data path's network stack, so the hint stays tightly ordered against the packets it describes (tighter than the out-of-band JSON-RPC channel). Overlapping windows extend the deadline monotonically. Documented in `docs/KEYFRAME_PRIORITY.md`. -* **Prometheus metrics endpoint.** A hand-rolled `/metrics` HTTP server (`metrics.rs`, `--metrics-bind`, no axum/hyper dependency) exports per-link and aggregate gauges plus the current mode. A shared stats layer (`stats.rs`, `subscriptions.rs`) backs both the metrics endpoint and the control socket subscriptions. - -## Testing and tooling - -* **`network-sim` crate.** A new workspace crate under `crates/network-sim` providing an integration-test harness, impairment models, scenario definitions, and topology helpers. -* **Network-namespace integration tests.** `tests/netns_basic.rs` (registration and forwarding), `tests/netns_failure.rs` (link failure and recovery), `tests/netns_impairment.rs` (adaptation to impairments), and `tests/netns_scenario.rs` (stability under evolving conditions). -* **CodeRabbit** review config added (`.coderabbit.yaml`). -* Test-only items moved from `#[allow(dead_code)]` to `#[cfg(test)]` gating, with new unit coverage for the CC state machine, batch regimes, weak-link gating, and the TOML config. - -## Housekeeping - -* Removed the bundled `receiver` symlink and the `.serena` memory files. -* Selection-strategy modules (`classic`, `enhanced`) made private. -* Connection-rotation tuning: `MIN_SWITCH_INTERVAL_MS` to 15, `STARTUP_GRACE_MS` to 5000. -* Dependency refreshes in `Cargo.lock`, plus `cargo fmt` and clippy cleanups across all targets. - -## Explored and removed (not in the current branch) - -These appear in the commit history between `v3.0.0` and `exp` but are **not present in the current tree**. They were prototyped, then dropped or superseded: - -* **EDPF scheduler (Earliest Delivery Path First), BLEST head-of-line guard, and IoDS reordering prevention.** The whole arrival-time-prediction scheduling pipeline was removed. No `edpf` / `blest` / `iods` modules exist; `congestion/` holds only `classic`, `enhanced`, and `mod`. -* **Shared bottleneck detection (RFC 8382).** Removed along with the EDPF pipeline it fed. -* **Connection exploration (`--exploration`, `set_exploration`).** Removed entirely: the flag, the JSON-RPC method, `sender/selection/exploration.rs`, and the `enable_explore` plumbing. The original version probed the *second-best* link (usually a healthy link already earning its own ACKs), fired every 30s whether or not anything was wrong, and had no budget. It diverted roughly half of all traffic for as long as its trigger held, surviving only because the switch cooldown happened to rate-limit it. It was rewritten as a bounded probe of *starved* links (one packet per link per 200ms) and then measured on the netem testbed against the scenario it exists for: a link gated to 0.00 Mbps, silently healed, then needed when the healthy link collapsed. It moved delivery 0.76 pts (Welch t=0.75, n=15), which is to say not at all. `GATED_LINK_PENALTY` already keeps a gated link *rankable* rather than excluded, so it retains a 0.2 to 0.5 Mbps trickle and re-adopts itself about 7s after healing, and the classifier's probation re-test covers the share-starvation latch on top of that. A mechanism that cannot be shown to help is debt, so it is gone rather than kept off by default. -* **RTT-threshold scheduling mode and the `edpf` mode.** `SchedulingMode` now has only `Classic` and `Enhanced`; the parser explicitly rejects `rtt-threshold` and `edpf`, and there is no `--rtt-delta-ms` flag in the CLI. `README.md` was updated to drop these modes (along with the stale `set_rtt_delta` and `mark_critical` control-socket examples), and `docs/RTT_THRESHOLD_SCHEDULING.md` was removed. diff --git a/README.md b/README.md index a70f51d2..848cc70a 100644 --- a/README.md +++ b/README.md @@ -57,60 +57,64 @@ The sender supports two mutually exclusive scheduling modes: This tool assumes that data is streamed from a SRT _sender_ in _caller_ mode to a SRT _receiver_ in _listener_ mode. To get any benefit over using SRT directly, the _sender_ should have 2 or more network links to the SRT listener (in the typical application, these would be internet-connected 4G modems). The sender needs to have [source routing](https://tldp.org/HOWTO/Adv-Routing-HOWTO/lartc.rpdb.simple.html) configured, as srtla uses `bind()` to map UDP sockets to a given connection. +## Install + +Each [release](https://github.com/irlserver/srtla_send/releases) ships a `.deb` for `amd64` and `arm64`. It runs on any Linux distribution with glibc 2.27 or newer (Ubuntu 18.04, Debian 10 and later): + +```bash +sudo dpkg -i srtla_*_arm64.deb +``` + +On other platforms, build from source. + ## Requirements -- **Rust nightly toolchain** and Cargo +- Rust and Cargo. `rust-toolchain.toml` pins the stable toolchain, and rustup installs it on the first `cargo` command. The minimum supported version is the `rust-version` in `Cargo.toml`. +- Rust nightly, only to format code: `rustfmt.toml` uses unstable options. - Unix (Linux/macOS) or Windows - Note: SIGHUP-based IP reload is Unix-only; Windows runs without that arm -**Important:** This project requires Rust nightly due to advanced rustfmt configuration options used in the codebase. - ## Build ```bash cd srtla_send -rustup install nightly -rustup default nightly # Set nightly as default for this project cargo build --release # binary at target/release/srtla_send ``` -Alternatively, you can use nightly for individual commands: - -```bash -cargo +nightly build --release -cargo +nightly fmt -cargo +nightly test -``` - ## Testing -The project includes comprehensive test suites covering unit tests, integration tests, and end-to-end tests. +The project includes test suites covering unit tests, integration tests, and end-to-end tests. ### Run Tests Locally +The `xlint` and `xtest` aliases in `.cargo/config.toml` run the same commands as CI, across every crate in the workspace. + ```bash -# Run all tests (requires nightly) -cargo test +# Run all tests in the workspace, as CI does +cargo xtest -# Run with verbose output -cargo test --verbose +# Run clippy, as CI does +cargo xlint -# Run specific test +# Run a specific test cargo test test_connection_score -# Check formatting (requires nightly) -cargo fmt --all -- --check +# Format code (nightly) +cargo +nightly fmt --all ``` ### CI/CD -The project uses GitHub Actions for continuous integration with automated testing on every push and pull request, including: +GitHub Actions runs on every push and pull request: + +- Formatting, clippy and a spell check +- Tests on Linux, Windows and macOS, plus Rust beta and a build on the minimum supported Rust version +- Miri over the unsafe batch-receive code +- Supply chain checks with cargo-deny +- The release debs, built against glibc 2.27 -- Multi-platform testing (Linux, Windows, macOS) -- Code formatting and linting checks -- Security vulnerability scanning -- Build verification across multiple Rust versions +Pushing a `vX.Y.Z` tag creates a draft release. See [RELEASING.md](RELEASING.md). ## Usage diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 00000000..b9c21508 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,64 @@ +# Releasing + +Releases are tag driven. Pushing a `vX.Y.Z` tag runs `.github/workflows/release.yml`. It builds both debs through the regular build workflow and creates a draft GitHub release. Nothing is public until you publish the draft by hand. + +## Steps + +1. Set `version = "X.Y.Z"` in the `[package]` table of the root `Cargo.toml`. This is the one source of truth: the deb names, the deb control file and `srtla_send -v` all read it. The member crates under `crates/` keep their own versions. + +2. Run `cargo build` so `Cargo.lock` picks up the new version, then commit both files: + + ``` + git commit -am "chore(srtla_send): bump version to X.Y.Z" + ``` + +3. Preview the release notes: + + ``` + scripts/changelog.sh HEAD + ``` + +4. Tag and push: + + ``` + git tag vX.Y.Z + git push origin main vX.Y.Z + ``` + +5. Wait for the Release workflow. If the tag and the `Cargo.toml` version disagree, it fails before it builds anything. On success it creates a draft release with these files: + + * `srtla_X.Y.Z_amd64.deb` + * `srtla_X.Y.Z_arm64.deb` + * `sha256sums.txt` + +6. Install each deb on a real device and stream through it before you publish. At minimum, the sender registers all uplinks and forwards data to a receiver. + +7. Read the release notes, edit anything that reads badly, then publish the draft. + +## The changelog + +`scripts/changelog.sh` groups every non-merge commit between the previous `v*` tag and the new one by its conventional commit type. `feat` goes under Features, `fix` under Bug fixes, then `perf`, `refactor`, `docs`, and `build` or `ci`. A commit with `!` or a `BREAKING CHANGE:` body goes to the top under Breaking changes, followed by the text after the marker. A subject that is not a conventional commit still appears, under Other changes. + +Commit subjects are the changelog, so write them for a reader. The script prints a scope in bold ahead of the subject (`**srtla-core:** ...`) and drops the `srtla_send` scope, because most commits have it. + +The release body starts with the install instructions in `.github/release-notes-header.md`. + +## Fixing a bad tag + +If the version check fails or an artifact is broken, delete the draft release in the GitHub UI and fix the problem. Then move the tag: + +``` +git tag -f vX.Y.Z +git push -f origin vX.Y.Z +``` + +## glibc floor + +`GLIBC_FLOOR` at the top of `.github/workflows/build.yml` sets the oldest glibc the debs support. `cargo zigbuild` links against that glibc version, so the runner's newer glibc does not leak into the binary. `scripts/package-deb.sh` checks the binary's glibc symbol versions against the floor and writes it into the deb's `Depends`. Raise the floor only to drop support for older distributions. + +To build a deb locally the same way CI does (needs `zig` and `cargo-zigbuild`): + +``` +cargo zigbuild --locked --profile release-lto --target aarch64-unknown-linux-gnu.2.27 +scripts/package-deb.sh target/aarch64-unknown-linux-gnu/release-lto/srtla_send arm64 2.27 dist +``` diff --git a/crates/srtla-core/src/priority.rs b/crates/srtla-core/src/priority.rs index 655bb722..263d7f9f 100644 --- a/crates/srtla-core/src/priority.rs +++ b/crates/srtla-core/src/priority.rs @@ -32,7 +32,7 @@ use std::sync::Arc; use std::sync::atomic::{AtomicU64, Ordering}; /// Magic byte identifying a priority-sidecar v1 datagram. Rejecting any -/// other leading byte lets us re-use the port for future framing later. +/// other leading byte lets us reuse the port for future framing later. pub const PROTO_MAGIC: u8 = 0xc1; /// Datagram length in bytes: `[magic u8][window_ms u32 big-endian]`. diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 00000000..9d27fc02 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,9 @@ +[toolchain] +# Pinned stable for building, linting and testing. Bump deliberately. +# Formatting is the one nightly exception: rustfmt.toml uses unstable options, +# so run `cargo +nightly fmt`. The minimal profile leaves rustfmt out of this +# toolchain, so a plain `cargo fmt` fails loudly instead of formatting with +# stable rules that CI then rejects. +channel = "1.97.1" +profile = "minimal" +components = ["clippy"] diff --git a/scripts/changelog.sh b/scripts/changelog.sh new file mode 100755 index 00000000..97a58288 --- /dev/null +++ b/scripts/changelog.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# +# Build release notes from the conventional commits in a tag range. +# +# GitHub's generate-notes only sees pull requests, and much of the work here +# lands as direct commits. Every commit subject is a conventional commit, which +# is enough to group a tag range into a readable changelog. +# +# Usage: scripts/changelog.sh [previous-tag] +# +# scripts/changelog.sh v4.1.0 # notes for a tag that already exists +# scripts/changelog.sh HEAD # preview what the next tag would say +# +# The previous tag defaults to the nearest v* tag reachable from 's +# parent. Markdown goes to stdout. + +set -euo pipefail + +tag="${1:?usage: changelog.sh [previous-tag]}" +prev="${2:-}" + +git rev-parse --verify --quiet "${tag}^{commit}" >/dev/null || { + echo "no such commit-ish: ${tag}" >&2 + exit 1 +} + +if [[ -z ${prev} ]]; then + prev="$(git describe --tags --abbrev=0 --match 'v[0-9]*' "${tag}^" 2>/dev/null || true)" +fi + +if [[ -n ${prev} ]]; then + range="${prev}..${tag}" +else + range="${tag}" +fi + +# Commit and compare links need the repository URL. Actions provides it; +# locally it comes from the origin remote in either SSH or HTTPS form. +if [[ -n ${GITHUB_REPOSITORY:-} ]]; then + repo_url="${GITHUB_SERVER_URL:-https://github.com}/${GITHUB_REPOSITORY}" +else + origin="$(git remote get-url origin 2>/dev/null || true)" + origin="${origin%.git}" + case "${origin}" in + # scp-style is git@host:owner/repo, so the colon is the path separator. + git@*) host_path="${origin#git@}" && repo_url="https://${host_path/://}" ;; + ssh://git@*) repo_url="https://${origin#ssh://git@}" ;; + http*) repo_url="${origin}" ;; + *) repo_url="" ;; + esac +fi + +# One file per section, because macOS still ships bash 3.2 and has no +# associative arrays. +sections="$(mktemp -d)" +trap 'rm -rf "${sections}"' EXIT + +# shellcheck disable=SC2016 # entries are Markdown, backticks are literal +entry_line() { + local scope="$1" subject="$2" sha="$3" short="${3:0:7}" line="" + + # Most commits are scoped to the binary itself, so that scope adds nothing. + if [[ -n ${scope} && ${scope} != "srtla_send" ]]; then + line="**${scope}:** " + fi + line+="${subject}" + if [[ -n ${repo_url} ]]; then + line+=" ([\`${short}\`](${repo_url}/commit/${sha}))" + else + line+=" (\`${short}\`)" + fi + printf -- '- %s\n' "${line}" +} + +# %x1f separates fields and %x1e separates commits, so a multi-line body stays +# attached to the commit it came from. +while IFS=$'\x1f' read -r -d $'\x1e' sha subject body; do + sha="${sha#$'\n'}" + [[ -n ${sha} ]] || continue + + type="" scope="" bang="" text="${subject}" + if [[ ${subject} =~ ^([A-Za-z]+)(\(([^\)]*)\))?(!)?:[[:space:]]+(.*)$ ]]; then + type="$(printf '%s' "${BASH_REMATCH[1]}" | tr '[:upper:]' '[:lower:]')" + scope="${BASH_REMATCH[3]}" + bang="${BASH_REMATCH[4]}" + text="${BASH_REMATCH[5]}" + fi + + if [[ -n ${bang} || ${body} == *"BREAKING CHANGE:"* ]]; then + entry_line "${scope}" "${text}" "${sha}" >>"${sections}/breaking" + # The text after the marker is the migration note, the one part of a + # commit message a reader needs inline. + printf '%s\n' "${body}" | + sed -n 's/^BREAKING CHANGE:[[:space:]]*/ - /p' >>"${sections}/breaking" + continue + fi + + case "${type}" in + feat) name=features ;; + fix) name=fixes ;; + perf) name=perf ;; + refactor) name=refactor ;; + docs) name=docs ;; + build | ci) name=build ;; + *) name=other ;; + esac + entry_line "${scope}" "${text}" "${sha}" >>"${sections}/${name}" +done < <(git log --no-merges --format="%H%x1f%s%x1f%b%x1e" "${range}") + +wrote=0 +section() { + [[ -s ${sections}/$2 ]] || return 0 + printf '### %s\n\n' "$1" + cat "${sections}/$2" + printf '\n' + wrote=1 +} + +section "Breaking changes" breaking +section "Features" features +section "Bug fixes" fixes +section "Performance" perf +section "Refactoring" refactor +section "Documentation" docs +section "Build and CI" build +section "Other changes" other + +if [[ ${wrote} -eq 0 ]]; then + printf 'No changes recorded for this release.\n\n' +fi + +if [[ -n ${repo_url} && -n ${prev} ]]; then + printf '**Full changelog**: %s/compare/%s...%s\n' "${repo_url}" "${prev}" "${tag}" +fi diff --git a/scripts/package-deb.sh b/scripts/package-deb.sh new file mode 100755 index 00000000..e41df161 --- /dev/null +++ b/scripts/package-deb.sh @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# +# Package a compiled srtla_send binary as the srtla .deb. +# +# Usage: scripts/package-deb.sh +# +# scripts/package-deb.sh target/aarch64-unknown-linux-gnu/release-lto/srtla_send arm64 2.27 dist +# +# The control file promises `libc6 (>= )`. Before it writes that +# promise, the script reads the glibc symbol versions the binary links against +# and refuses to package it if any of them is newer than the floor. A binary +# built on a new runner without cargo-zigbuild's glibc pin fails here, not on +# a user's device. + +set -euo pipefail + +usage="usage: package-deb.sh " +bin="${1:?${usage}}" +arch="${2:?${usage}}" +floor="${3:?${usage}}" +out="${4:?${usage}}" + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +version="$("${root}/scripts/version.sh")" + +# readelf, unlike objdump, reads any ELF architecture, so the x86 runner can +# inspect the arm64 binary. +needed="$(readelf --version-info --wide "${bin}" | + grep -o 'GLIBC_[0-9][0-9.]*' | sed 's/^GLIBC_//' | sort -uV | tail -n1)" +if [[ -z ${needed} ]]; then + echo "${bin}: no versioned glibc symbols found; is this a glibc binary?" >&2 + exit 1 +fi +if [[ "$(printf '%s\n%s\n' "${needed}" "${floor}" | sort -V | tail -n1)" != "${floor}" ]]; then + echo "${bin} needs glibc ${needed}, newer than the ${floor} floor the package promises." >&2 + echo "Build it with cargo zigbuild --target .${floor}." >&2 + exit 1 +fi +echo " ok ${bin} needs glibc ${needed} (floor ${floor})" + +staging="$(mktemp -d)" +trap 'rm -rf "${staging}"' EXIT +# mktemp -d creates 0700, and the staging root becomes the package's `./`. +chmod 755 "${staging}" + +install -Dm755 "${bin}" "${staging}/usr/bin/srtla_send" +mkdir -p "${staging}/DEBIAN" +cat >"${staging}/DEBIAN/control" < +Description: SRT transport proxy with link aggregation for connection bonding +Depends: libc6 (>= ${floor}) +Homepage: https://irlserver.com +Section: net +Priority: optional +EOF + +mkdir -p "${out}" +deb="${out}/srtla_${version}_${arch}.deb" +# CI builds as an unprivileged runner user; without --root-owner-group the +# installed binary would belong to that uid. +dpkg-deb --root-owner-group --build "${staging}" "${deb}" +echo " ok ${deb}" diff --git a/scripts/version.sh b/scripts/version.sh new file mode 100755 index 00000000..07dba759 --- /dev/null +++ b/scripts/version.sh @@ -0,0 +1,19 @@ +#!/usr/bin/env bash +# +# Print the srtla_send version from the [package] table of Cargo.toml. +# +# Read with sed instead of `cargo metadata` so the release workflow can check a +# tag against it without first installing the pinned toolchain. + +set -euo pipefail + +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +version="$(sed -nE '/^\[package\]/,/^\[/ s/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/p' "${root}/Cargo.toml" | head -n1)" + +if [[ -z ${version} ]]; then + echo "Cargo.toml: no version found in the [package] table" >&2 + exit 1 +fi + +printf '%s\n' "${version}" diff --git a/src/main.rs b/src/main.rs index a069e28c..c0621efe 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ // The CLI is a thin shell over the `srtla_send` library: it parses arguments, // starts the optional sidecars, picks an egress binder for the platform, and // hands off to `sender::run_sender_with_config`. Consuming the library rather -// than re-declaring its modules keeps one compilation of the tree instead of +// than redeclaring its modules keeps one compilation of the tree instead of // two, and keeps the library's embedder-facing surface (the Android and Apple // binders no CLI ever constructs) from reading as dead code here. use anyhow::{Context, Result};