diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..2e398e1 --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,175 @@ +# Build and test lvpm on each platform's own runner, publish the executables, +# and run lvpm against LabVIEW for Linux in NI's container (docs/linux.md). +name: build + +# Every branch, so a feature branch is tested before it is merged. Tags are +# left out. +on: + push: + branches: ['**'] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +# A newer push to a branch makes its running build moot. On main each commit +# gets a group of its own, so none is cancelled or dropped from the queue. +concurrency: + group: ${{ github.workflow }}-${{ github.ref == 'refs/heads/main' && github.sha || github.ref }} + cancel-in-progress: true + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + name: build (${{ matrix.name }}) + strategy: + fail-fast: false + matrix: + include: + # Windows 64-bit: the MSVC build the releases ship + - os: windows-latest + target: x86_64-pc-windows-msvc + name: windows-x64 + exe: lvpm.exe + + # Linux 64-bit, static: NI's image is Ubuntu 22.04, older than the + # runner's glibc + - os: ubuntu-latest + target: x86_64-unknown-linux-musl + name: linux-x64 + exe: lvpm + + runs-on: ${{ matrix.os }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + + - name: Install musl tools + if: runner.os == 'Linux' + run: sudo apt-get update && sudo apt-get install -y musl-tools + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo index + uses: actions/cache@v4 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-git-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache target directory + uses: actions/cache@v4 + with: + path: target + key: ${{ runner.os }}-${{ matrix.target }}-target-${{ hashFiles('Cargo.lock') }} + + # On the runner's own platform: Windows tests on Windows. + - name: Unit tests + run: cargo test --workspace + + # lvpm and the end-to-end runner (e2e/), which runs in NI's container + # and so is static on Linux as well. + - name: Build release + run: cargo build --release --workspace --target ${{ matrix.target }} + env: + CC_x86_64_unknown_linux_musl: musl-gcc + + - name: Upload executable + uses: actions/upload-artifact@v4 + with: + name: lvpm-${{ matrix.name }} + path: target/${{ matrix.target }}/release/${{ matrix.exe }} + if-no-files-found: error + + - name: Upload end-to-end runner + uses: actions/upload-artifact@v4 + with: + name: lvpm-e2e-${{ matrix.name }} + path: target/${{ matrix.target }}/release/lvpm-e2e${{ runner.os == 'Windows' && '.exe' || '' }} + if-no-files-found: error + + # Both executables in one download, named for the version and commit, to + # hand to testers. The Linux one is a tarball: an artifact zip does not keep + # the executable bit. + collect: + needs: build + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Download executables + uses: actions/download-artifact@v4 + with: + path: executables + + - name: Name them + run: | + version=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1) + mkdir dist + cp executables/lvpm-windows-x64/lvpm.exe "dist/lvpm-$version-windows-x64.exe" + chmod +x executables/lvpm-linux-x64/lvpm + tar -czf "dist/lvpm-$version-linux-x64.tar.gz" -C executables/lvpm-linux-x64 lvpm + echo "BUNDLE=lvpm-$version-${GITHUB_SHA::7}" >> "$GITHUB_ENV" + ls -l dist + + - name: Upload combined artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ env.BUNDLE }} + path: dist/ + if-no-files-found: error + + # Both suites of the end-to-end runner in e2e/ (local packages, then LUnit + # and G-Image from the public indexes), each in a fresh container, with the + # same steps on each platform: only the runner and the image differ. + # + # Windows runs on windows-2022, not windows-latest: the image is built on + # Server 2022 (10.0.20348) and runs there with process isolation; Server + # 2025 would need Hyper-V. Pulling it is most of that job, 10 to 16 minutes + # for 19.7 GB on disk, so a push to a branch other than main leaves Windows + # out: it runs on pull requests, on main, and by hand. (A job's `if` cannot + # see the matrix, hence the list built here.) + e2e: + name: e2e (${{ matrix.platform }}) + needs: build + strategy: + fail-fast: false + matrix: + platform: ${{ fromJSON((github.event_name != 'push' || github.ref == 'refs/heads/main') && '["linux-x64", "windows-x64"]' || '["linux-x64"]') }} + runs-on: ${{ matrix.platform == 'windows-x64' && 'windows-2022' || 'ubuntu-latest' }} + timeout-minutes: 60 + env: + # Pinned, so a new image is a change someone makes on purpose. + IMAGE: nationalinstruments/labview:2026q3-${{ matrix.platform == 'windows-x64' && 'windows' || 'linux' }} + LVPM: dist/lvpm${{ matrix.platform == 'windows-x64' && '.exe' || '' }} + steps: + - name: Download lvpm + uses: actions/download-artifact@v4 + with: + name: lvpm-${{ matrix.platform }} + path: dist + + - name: Download the end-to-end runner + uses: actions/download-artifact@v4 + with: + name: lvpm-e2e-${{ matrix.platform }} + path: dist + + - name: lvpm against LabVIEW 2026 + shell: bash + run: | + chmod +x dist/* + dist/lvpm-e2e run --image "$IMAGE" --lvpm "$LVPM" diff --git a/Cargo.lock b/Cargo.lock index 7d53acb..c569736 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -19,6 +19,15 @@ dependencies = [ "cpufeatures", ] +[[package]] +name = "aho-corasick" +version = "1.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba" +dependencies = [ + "memchr", +] + [[package]] name = "anstream" version = "1.0.0" @@ -75,6 +84,21 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "assert_cmd" +version = "2.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2aa3a22042e45de04255c7bf3626e239f450200fd0493c1e382263544b20aea6" +dependencies = [ + "anstyle", + "bstr", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -126,6 +150,17 @@ dependencies = [ "zeroize", ] +[[package]] +name = "bstr" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f" +dependencies = [ + "memchr", + "regex-automata", + "serde_core", +] + [[package]] name = "bumpalo" version = "3.20.3" @@ -345,6 +380,12 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.11.3" @@ -381,6 +422,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" +[[package]] +name = "escape8259" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f1f0d310082fd5de539d4f41601182241147498444a7695c520288a0cdbead6" + [[package]] name = "fallible-iterator" version = "0.3.0" @@ -887,6 +934,18 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "libtest-mimic" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14e6ba06f0ade6e504aff834d7c34298e5155c6baca353cc6a4aaff2f9fd7f33" +dependencies = [ + "anstream", + "anstyle", + "clap", + "escape8259", +] + [[package]] name = "litemap" version = "0.8.2" @@ -922,6 +981,19 @@ dependencies = [ "zip", ] +[[package]] +name = "lvpm-e2e" +version = "0.1.0" +dependencies = [ + "anyhow", + "assert_cmd", + "libtest-mimic", + "regex", + "serde_json", + "sha2", + "zip", +] + [[package]] name = "lzma-rust2" version = "0.16.5" @@ -1041,6 +1113,33 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24" +[[package]] +name = "predicates" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ada8f2932f28a27ee7b70dd6c1c39ea0675c55a36879ab92f3a715eaa1e63cfe" +dependencies = [ + "anstyle", + "difflib", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cad38746f3166b4031b1a0d39ad9f954dd291e7854fcc0eed52ee41a0b50d144" + +[[package]] +name = "predicates-tree" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0de1b847b39c8131db0467e9df1ff60e6d0562ab8e9a16e568ad0fdb372e2f2" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "proc-macro2" version = "1.0.107" @@ -1148,6 +1247,35 @@ dependencies = [ "rand_core", ] +[[package]] +name = "regex" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4" + [[package]] name = "reqwest" version = "0.13.4" @@ -1563,6 +1691,12 @@ dependencies = [ "syn 2.0.119", ] +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + [[package]] name = "thiserror" version = "2.0.19" @@ -1815,6 +1949,15 @@ version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + [[package]] name = "walkdir" version = "2.5.0" diff --git a/Cargo.toml b/Cargo.toml index 9706e55..826f72b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,11 @@ edition = "2024" description = "Open-source package manager for LabVIEW (.vip) packages" license = "MIT OR Apache-2.0" repository = "https://github.com/Flydroid/lvpm" + +# e2e/ is the end-to-end test runner for NI's LabVIEW containers; it is not +# part of a release, and a plain `cargo test` here does not build it. +[workspace] +members = ["e2e"] [dependencies] anyhow = "1.0.104" clap = { version = "4.6.6", features = ["derive"] } diff --git a/README.md b/README.md index 0754515..e61e340 100644 --- a/README.md +++ b/README.md @@ -33,8 +33,10 @@ Not affiliated with JKI or NI. Working, in active development; see ## Install lvpm -Windows 10/11 with LabVIEW 2020 or later. Linux target detection exists but -is untested. +Windows 10/11 with LabVIEW 2020 or later. On Linux, lvpm is verified against +LabVIEW 2026 Q3 in NI's container image; take `lvpm--linux-x64.tar.gz` +from the artifacts of a CI run, or build it from source, and see +[docs/linux.md](docs/linux.md) for what differs there. ### With the install script @@ -357,6 +359,11 @@ differently in the container than on your machine, `--hooks` is the first thing to try; it starts a headless LabVIEW for them. `--relink` likewise when the job's output _is_ relinked packages. +NI's Linux image (`nationalinstruments/labview:2026q3-linux`) works the same +way; [docs/linux.md](docs/linux.md) has the differences, and +[.github/workflows/build.yml](.github/workflows/build.yml) runs lvpm in it on +every push. + Two container facts worth knowing. LabVIEW allows one mode at a time per machine, headless or IDE, so a headless job cannot share a host with an open IDE. And any LabVIEW that is started (by `--hooks`, `--relink`, or the job @@ -439,7 +446,9 @@ runs. but runs to completion reports as ok. - A failure part-way through unpacking leaves files behind with no manifest, and one failed download aborts the rest of the plan. -- Windows is the tested platform. +- Windows is the tested platform. Linux is verified against LabVIEW 2026 Q3 + in NI's container image only, and does not yet honour a package's + `Exclusive_OS` ([docs/linux.md](docs/linux.md)). - In a venv, install hooks are extracted but never run (they act on the LabVIEW installation, not on an overlay), and file groups aimed at machine locations (``, ``) are skipped. Both are recorded in the @@ -468,6 +477,7 @@ runs. | `version.rs` | Package version ordering: OGPT `version-release`, VIPM's four-part form; not semver | | `src/lv-src/relink-package.vi` | The relink VI embedded into the executable at build time (LabVIEW 2020) | | `scripts/install.ps1` | Download, verify and install the latest release | +| `e2e/` | `lvpm-e2e`: end-to-end checks against LabVIEW for Linux and for Windows in NI's container images, with local test packages and with public ones (the LabVIEW tree must come out of install and uninstall unchanged) | ## License diff --git a/docs/linux.md b/docs/linux.md new file mode 100644 index 0000000..fa60b8a --- /dev/null +++ b/docs/linux.md @@ -0,0 +1,185 @@ +# LabVIEW on Linux + +lvpm builds and runs on Linux, and drives LabVIEW for Linux the way it drives +the Windows IDE: files copied, then relinked and hooks run over VI Server. +Everything below was measured on 2026-09-26 against NI's container image +`nationalinstruments/labview:2026q3-linux` (LabVIEW 2026 Q3, Ubuntu 22.04). +The end-to-end runner in [e2e/](../e2e/src/main.rs) repeats every check in a +throwaway container in about two and a half minutes, and [CI](#ci) runs it, +on Windows as well. + +## Building + +```console +$ cargo build --release # target/release/lvpm, for this machine's glibc +$ cargo test +$ rustup target add x86_64-unknown-linux-musl +$ CC_x86_64_unknown_linux_musl=musl-gcc cargo build --release --target x86_64-unknown-linux-musl +``` + +Nothing in the dependency tree needs OpenSSL: reqwest's `default-tls` is +rustls on aws-lc, which needs only a C compiler (`musl-gcc` for the static +build). A binary built on a current distribution does not start in NI's image +(`GLIBC_2.38 not found`; the image has 2.35), so the static musl build is the +one to ship. + +### Windows stays as it was + +The platform is chosen when lvpm is compiled, not when it runs: each +difference below is a `#[cfg(windows)]` / `#[cfg(target_os = "linux")]` +pair, or `#[cfg(not(windows))]` where the Linux half holds on any POSIX +system (separators, line ends, the per-user cache), and the Windows half is +the code that was there before. Other platforms do not build (see the +roadmap's macOS section). What a Windows user can +see changed is small: the "VI Server is disabled" and "no LabVIEW.exe" +messages name the full path of the file, and a `Target Dir` such as +`/addons/Foo` now expands to `...\vi.lib\addons\Foo` instead of +`...\vi.lib\addons/Foo` (the same path either way). Tests that pin +Windows behaviour are `cfg(windows)`, with Linux counterparts, and run on a +Windows runner in [CI](#ci). + +## What differs + +| | Windows | Linux, measured | lvpm on Linux | +|---|---|---|---| +| Installation | registry; `LabVIEW.exe` | `/usr/local/natinst/LabVIEW--64`, Q1 and Q3 of a year sharing it; `/etc/natinst/labview--64` links to its `etc`, whose `labview.dir` names it. `labview` in it is a symlink to the edition's binary (`labviewprofull`) | reads `/etc/natinst`, scans `/usr/local/natinst`, requires `labview` | +| Internal version | registry key (`26.3`) | not in the directory name; `readme/UNINSTALL` in the install sets `LV_MAJOR_VER=26`, `LV_MINOR_VER=3` | reads it; the year when it is missing | +| Preferences | `LabVIEW.ini` beside the exe | `~/natinst/.config/LabVIEW-/.conf`, per user, named after the name LabVIEW was started as (`labview.conf` for `labview`). Absent until the user's first launch; LabVIEW adds the `[LabVIEW]` header and writes LF | reads `labview.conf`; a venv ini starts from `[LabVIEW]` when there is none | +| VI Server default | on | **off**: without `server.tcp.enabled=True` LabVIEW never listens. NI's image seeds all three `.conf` files with it | absent means off, and the error names the file, instead of a 120 s wait | +| VI Server port | 3363, 3364 on 2026 | 3363 on 2026 Q3 | read from the `.conf` | +| Protocol | | unchanged: handshake, error 63 until initialised, open, run, save, control values | | +| Paths on the wire (`PTH0`) | drive letter first | no drive and no root component: `/usr/local` is `usr`, `local` | decoded to `/usr/local/...`, not `usr\local\...` | +| Relink folder | `C:\...` | `/...`. Handed the backslashed form, the relink VI walks nothing and reports success: 0 items where the same folder gives 2 | sent as it is | +| Palette and menu refresh | two shipping VIs | the same VIs at the same relative paths | joined a component at a time (a `\` inside one `join` separates only on Windows) | +| `` tokens | environment variables | what `Get System Directory.vi` answers (below) | the same table | +| Records of a global install | `%ProgramData%\lvpm` | `/var/lib/lvpm//` | (was a `C:\ProgramData` directory under the working directory) | +| Index cache | `%LOCALAPPDATA%\lvpm\cache` | `$XDG_CACHE_HOME/lvpm`, else `~/.cache/lvpm` | | +| Relink VI, staged PostUninstall | `%TEMP%` | `/tmp` is shared, and a name another user wrote first is one this user cannot write | the per-user cache | +| Target Dir tails | either separator | `\addons\Foo` would be one directory named `addons\Foo` | split on both | +| Headless | `LV_RTE_HEADLESS=1` (2026 and later) | the same switch, also from 2026. LabVIEW starts its own `Xvfb :99` and needs no `DISPLAY`; the log is `/tmp/labview__headless__cur.txt` | the hint names it | +| `-pref ` | yes | yes: a second instance on its own ini and port runs beside the first and shares its Xvfb. The Linux binary has no `AllowMultipleInstances` key | unchanged | +| LVAddons | `C:\Program Files\NI\LVAddons` | `/usr/local/natinst/share/lvaddons` (NI's VI Analyzer support ships there). `LVAddons.AdditionalLocations` is honoured | the venv path goes with forward slashes | +| Restarting on a port | immediate | **a LabVIEW started while its port holds a TIME_WAIT connection (up to 60 s after the last one closed) never serves VI Server**, and never retries | waits for the port, 65 s at most, before starting LabVIEW | +| Ctrl+C | IDE detached | a child in lvpm's process group gets the terminal's SIGINT | LabVIEW gets a process group of its own | + +### `` tokens + +The tokens are the directory types of LabVIEW's `Get System Directory.vi` by +name. lvpm resolves them to what that VI returned over VI Server on Linux, run +as root: + +| Token | Linux | +|---|---| +| `` | `$HOME/Documents` | +| `` | `$HOME/Desktop` | +| `` | `$HOME` | +| ``, ``, `` | `/usr/local` | +| `` | `/usr/lib` | +| `` | `/` | +| `` | `/tmp` | + +The VI's remaining types, for reference: User Preferences `$HOME`, Public +Preferences `/etc`, System Installed Libraries `/usr/local/lib`, Public Cache +`/var/cache`. + +### LabVIEWCLI + +LabVIEWCLI on Linux wants `-LabVIEWPath`, the operation name first and +`-Headless` last; given `-Headless` in between, it prints its usage and exits +0. A clean mass compile says `MassCompile operation succeeded` and exits 0; a +broken VI shows as `### Bad VI:` in the log, with exit code 3. For each +project in the folder, the compile writes `.aliases` and +`..UserState/` beside it. + +## Verified + +With `lvpm-e2e run --image nationalinstruments/labview:2026q3-linux --lvpm +`, all in one container (both executables built for +`x86_64-unknown-linux-musl`): + +- `lvpm targets` reports `LabVIEW 2026 (64-bit) v26.3`. +- A headless `lvpm install --relink --hooks` in a project with no venv starts + LabVIEW, runs PreInstall, copies into `vi.lib` and `$HOME/Documents`, + relinks and runs PostInstall. Records land in `/var/lib/lvpm`. +- `list`, `refresh` (both VIs run), `relink --all`, `run-hooks` and + `uninstall --all` work, and the uninstall prunes what it created. +- `vi-save` rewrites a LabVIEW 2020 VI as 26.3. A caller whose subVI moved is + found, relinked and saved with a `` link. +- In a venv: `venv create`, then `install --relink` in a LabVIEW started with + `-pref` on the venv's port, then `launch` straight after. The overlay works: + a VI linked to `/lvpm_linux_test/...` runs on the venv's LabVIEW and + is broken (error 1003) on a fresh plain one. + +By hand: as a non-root user with no `.conf`, `lvpm start` stops at once with +the "VI Server is disabled" message, and the whole venv flow works without +root. + +## CI + +[.github/workflows/build.yml](../.github/workflows/build.yml), on every push +to any branch, on every pull request (which tests the merge with its base), +and by hand. A newer push to a branch other than `main` cancels its run still +in progress. + +- **build** — one job per platform on its own runner: `windows-latest` + (`x86_64-pc-windows-msvc`) and `ubuntu-latest` (static + `x86_64-unknown-linux-musl`). Each runs `cargo test` and a release build, + and uploads its executable and the end-to-end runner, `lvpm-e2e`. +- **collect** — puts both executables in one artifact, + `lvpm--`, for handing to testers. +- **e2e (linux-x64)**, **e2e (windows-x64)** — one job, the same steps on + each platform: `lvpm-e2e` with that platform's executable, in + `nationalinstruments/labview:2026q3-linux` or `2026q3-windows` (pinned), + each of its two suites in a fresh container: + - **local** — the checks under [Verified](#verified), with two packages + made from the VIs in `tools/`. + - **public** — LUnit and G-Image from the public indexes: a headless + `install --hooks` (G-Image needs its PostInstall) in which every hook + must run and every recorded file must be on disk, `relink --all`, an + uninstall after which no file in the LabVIEW tree is added, gone or + changed, and the same set relinked in a venv, which must leave the + LabVIEW tree alone. + + Windows runs on `windows-2022`, whose Server 2022 matches the image and + runs it with process isolation. The pull is most of that job: 10 to 16 + minutes for 19.7 GB on disk, so it runs on pull requests, on `main` and by + hand, and a push to another branch tests Linux only. + + There is no mass compile. A bad VI in a third-party package (LUnit ships + one on purpose) or in LabVIEW's own templates says nothing about lvpm, and + the compile writes project files into the tree the uninstall check + compares. + +## Not done + +What Linux still needs, most important first. + +1. **OS gates on packages.** lvpm ignores `Exclusive_OS`, both in the index + (`Platform.Exclusive_OS`) and per file group, so on Linux it would install + a package that declares itself Windows-only. LabVIEW 2026 for Linux ships + NI's Wine layer (`ni-wine`, `ni-wine-dotnet-runtime-80`, + `ni-dotnetcore-interface`); what that means for .NET nodes is not measured. +2. **Root.** The installation is owned by root. A global install needs write + access to it, and the relink and hooks run in a LabVIEW that has to save + into it. Under `sudo`, Ubuntu sets `$HOME` to root's, so lvpm reads root's + `labview.conf` and starts LabVIEW as root. A venv needs neither. lvpm should + check write access up front and say so. +3. **Case.** Linux is case-sensitive; much of what lvpm and packages were + written against is not. `install.rs` looks a manifest up by the name as + typed, so `uninstall OGLib_Error` misses. Archive entries are matched + case-insensitively but written in the spec's case. `venv::port_for` + lowercases the path, so two repos that differ only in case share a port. + And a VI that links a subVI in a different case does not load on Linux. +4. **Releases.** CI builds both executables; a tagged release still carries + only Windows, and there is no `install.sh` to match `install.ps1`. +5. **Not measured.** LabVIEW for Linux before 2026 (the `.conf` directory + name, `readme/UNINSTALL`, `LVAddons.AdditionalLocations` from 2024 Q1 on); + other distributions (RHEL, openSUSE); Community edition; an interactive IDE + on a real display. Headless mode arrived with LabVIEW 2026: the 2025 Q3 + binary has no `LV_RTE_HEADLESS` and no `--headless` (its image sets + `EnableCICDFeaturesForLabVIEW` instead), so lvpm cannot tell that a 2025 + machine is headless. +6. **Containers.** Start the container with `--init`. A LabVIEW that lvpm + started is reparented to PID 1, and `sleep infinity` never reaps it. The + TIME_WAIT rule applies to anything that restarts LabVIEW on a port within a + minute, LabVIEWCLI included. diff --git a/docs/roadmap.md b/docs/roadmap.md index b33154c..fe04829 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -214,9 +214,81 @@ Toolkits\Caraya\Caraya CLI.vi" -- -s -x ` — lvpm's own tooling on packages, which is the reading of "modifying LabVIEW code" to settle before that step runs on shared infrastructure. +## Integration test VIs + +The container jobs test lvpm with packages it does not control: the +`local` suite of `lvpm-e2e` with the VIs in `tools/`, the `public` suite +with LUnit and G-Image as a reference. Neither was made to test an +installer: LUnit ships a VI broken on purpose, so `public` cannot ask whether +a package's VIs +work, only whether lvpm put them in place and took them out again. A package +of VIs made for the job would test exactly what lvpm answers for, on Windows +and Linux alike: + +- a caller whose subVI sits in another folder of the package, which relink + must fix, and a caller outside the package that links into ``, for + the venv overlay; +- PreInstall and PostInstall hooks that write a marker file with what they + were handed (package name, files installed); today's hook VI only shows + that it ran. One of them does setup the package cannot work without, as + G-Image's does; +- a class, a library, a malleable VI, an LLB and a project, each once; +- a palette `.mnu`, an `` group and a Target Dir written + with backslashes; +- a file group into a folder LabVIEW itself uses, such as + `/Framework/Providers/`, and a file directly in such a + folder, as LUnit's provider library is, where relink must stay inside the + package's own files; +- nothing broken, so the job can run every VI (`vi-run`) and fail on any + error. + +Saved in LabVIEW 2020, like the relink VI, and zipped into a `.vip` by the +test scripts as today. + +## macOS + +Not supported, and a macOS build stops with a `compile_error!` in `main.rs`. +What is a fact about LabVIEW on Linux is `cfg(target_os = "linux")`; what is +true of any POSIX system is `cfg(not(windows))` or `cfg(unix)` and needs no +new branch. + +- **What carries over from Linux:** forward slashes, LF ini files, the + per-user cache, LabVIEW in its own process group, the VI Server client, + venvs. +- **What needs its own branch, each to be measured on a Mac with LabVIEW:** + the install location (likely `/Applications/National Instruments/LabVIEW + 64-bit/`), the executable inside the `.app` bundle, the preferences + file (likely under `~/Library/Preferences/`), the internal version (likely + the bundle's `Info.plist`), the `` tokens (run `Get System + Directory.vi`, as for Linux), `PTH0` paths (the first component may be the + volume name), where install records go, and whether a TIME_WAIT port blocks + VI Server as it does on Linux. +- **The obstacle is testing:** NI publishes no macOS container, so there is + no equivalent of the container end-to-end checks in CI without a macOS runner that + has a licensed LabVIEW. + ## Housekeeping - `Replace Mode = If Newer` compares timestamps instead of "write when absent". - Read hook VIs' `error out` back. - A failure part-way through unpacking leaves files behind with no manifest. -- Linux target detection exists but is untested. +- Relink saves LabVIEW's own files. The relink VI takes folders and walks + their subfolders too, and a package file that lies directly in a folder + LabVIEW fills puts that whole folder in the walk. LUnit's + `LUnit Project Provider.lvlib` lies in `resource/Framework/Providers`; a + single test VI there made relink save 44 of LabVIEW's files (Actor + Framework and AppBuilder providers), still changed after the uninstall. + The `public` suite failed on this. Same code on Windows. Fix: a relink VI + that takes the package's files, or no walk of a folder that holds files + of others. +- Uninstall leaves the `.aliases` and `.UserState` files that LabVIEW writes + beside a package's project (a mass compile of its folder writes them), and + with them the package's folders. +- `relink --all` never marks a package with nothing to relink, while + `install --relink` does, so `lvpm list` shows such a package + `NOT relinked` for good. +- Linux: install, relink, hooks, refresh and venvs verified against LabVIEW + 2026 Q3 in NI's container ([linux.md](linux.md)), and run in CI + (`.github/workflows/build.yml`). Still open there: `Exclusive_OS` gates, + root ownership of the install tree, case sensitivity, a Linux release + artifact. diff --git a/docs/vi-server-protocol.md b/docs/vi-server-protocol.md index ee71e47..755d19b 100644 --- a/docs/vi-server-protocol.md +++ b/docs/vi-server-protocol.md @@ -146,7 +146,13 @@ encoder serves both the wire and any offline path rewriting. An absolute Windows path contributes its drive letter as the first component: `C:\Git\lvpm\tools\X.vi` → `01 "C" 03 "Git" 04 "lvpm" 05 "tools" 06 "X.vi"`. -The length field covers the count field plus the components. +The length field covers the count field plus the components. The count field +is really a `u16` kind (0 absolute, 1 relative, 2 not a path) and a `u16` +count; an absolute path's kind is 0, so the two read the same. + +On Linux there is no drive and no root component: LabVIEW 2026 Q3 for Linux +sends and takes `/usr/local/x.vi` as `03 "usr" 05 "local" 04 "x.vi"`, kind 0. +Decoded, such a record has to be rooted again, or it reads as a relative path. ## `GetVIRef` (3) diff --git a/e2e/Cargo.toml b/e2e/Cargo.toml new file mode 100644 index 0000000..d9da6c2 --- /dev/null +++ b/e2e/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "lvpm-e2e" +version = "0.1.0" +edition = "2024" +description = "End-to-end checks of lvpm against LabVIEW in NI's container images" +license = "MIT OR Apache-2.0" +publish = false + +[dependencies] +anyhow = "1.0.104" +assert_cmd = "2.2.2" +libtest-mimic = "0.8.2" +regex = "1.13.1" +serde_json = "1.0.151" +sha2 = "0.11.0" +zip = "8.6.0" diff --git a/e2e/src/checks.rs b/e2e/src/checks.rs new file mode 100644 index 0000000..9697a8f --- /dev/null +++ b/e2e/src/checks.rs @@ -0,0 +1,280 @@ +//! In the container: what every suite's checks share. + +use std::fmt::Display; +use std::path::{Path, PathBuf}; +use std::process::{Command, ExitCode}; +use std::sync::OnceLock; +use std::time::Duration; + +use anyhow::{Context, Result}; +use assert_cmd::assert::Assert; +use libtest_mimic::{Arguments, Failed, Trial}; +use sha2::{Digest, Sha256}; + +use crate::{EXE, MARKER, Suite, output}; + +pub type Check = Result<(), Failed>; + +/// Where things are in the container, settled once before the first check. +pub struct Env { + pub lvpm: PathBuf, + pub work: PathBuf, + /// The LabVIEW install folder, as `lvpm targets` reports it. + pub lv: PathBuf, +} + +static ENV: OnceLock = OnceLock::new(); + +pub fn env() -> &'static Env { + ENV.get().expect("set before the checks run") +} + +pub fn run(suite: Suite, args: Vec) -> ExitCode { + let mut args = Arguments::from_iter( + std::iter::once(format!("lvpm-e2e checks {}", suite.name())).chain(args), + ); + // In order, one at a time: each check builds on the ones before it. + args.test_threads = Some(1); + + if std::env::var_os(MARKER).is_none() && !args.list { + eprintln!( + "lvpm-e2e checks runs in the container `lvpm-e2e run` starts: it uninstalls everything" + ); + return ExitCode::from(2); + } + if !args.list { + match setup() { + Ok(e) => drop(ENV.set(e)), + Err(e) => { + eprintln!("lvpm-e2e: {e:#}"); + return ExitCode::FAILURE; + } + } + } + let trials = match suite { + Suite::Local => crate::local::trials(), + Suite::Public => crate::public::trials(), + }; + libtest_mimic::run(&args, trials).exit_code() +} + +fn setup() -> Result { + let here = std::env::current_exe()?; + let work = here + .parent() + .context("executable has a folder")? + .to_path_buf(); + let lvpm = work.join(format!("lvpm{EXE}")); + let targets = output(&lvpm.to_string_lossy(), &["targets"])?; + // `