diff --git a/.github/workflows/_test-user-setup.yml b/.github/workflows/_test-user-setup.yml index 476aa76..3f64178 100644 --- a/.github/workflows/_test-user-setup.yml +++ b/.github/workflows/_test-user-setup.yml @@ -43,8 +43,8 @@ jobs: node CI/generate.js \ --distro humble \ --variant ros-base \ - --tools "bashrc,locale,sudo" \ - --usertype custom \ + --tools "bashrc,locale,sudo,zsh" \ + --usertype user \ --username ros-dev \ --uid 1000 \ --out ./build-context @@ -77,6 +77,7 @@ jobs: -e EXPECTED_USER=ros-dev \ -e EXPECTED_UID=1000 \ -e EXPECT_SUDO=true \ + -e EXPECT_ZSH=true \ -v "${{ github.workspace }}/CI/validate.sh:/validate.sh:ro" \ ros2-test-user-custom:ci \ bash /validate.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 21a1794..efbc0a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -58,30 +58,33 @@ jobs: base: ${{ github.base_ref || 'main' }} filters: | generator: + - 'src/**' - 'CI/generate.js' - 'CI/validate.sh' - '.github/workflows/ci.yml' - '.github/workflows/_test-base.yml' user_setup: + - 'src/**' - 'CI/generate.js' - 'CI/validate.sh' - '.github/workflows/_test-user-setup.yml' gui: + - 'src/**' - 'CI/generate.js' - 'CI/validate.sh' - '.github/workflows/_test-gui.yml' nvidia: + - 'src/**' - 'CI/generate.js' - 'CI/validate.sh' - '.github/workflows/_test-nvidia.yml' any: - 'src/**' - - 'data/**' - - 'bin/**' - 'CI/**' - '.github/workflows/**' - 'index.html' - 'README.md' + - 'tests/**' # ── Version Consistency ────────────────────────────────────── version-check: @@ -92,9 +95,18 @@ jobs: - uses: actions/setup-python@v5 with: python-version: '3.12' + - uses: actions/setup-node@v4 + with: + node-version: '20' - name: Run Version Check run: | PYTHONPATH=src python3 tests/test_version.py + - name: Run Output Shape Tests + run: | + PYTHONPATH=src python3 tests/test_output_shape.py + - name: Run Web Bundle Tests + run: | + PYTHONPATH=src python3 tests/test_web_bundle.py # ── Python Build Verification ───────────────────────────────── build-check: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 9792162..d05cefe 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -26,6 +26,8 @@ jobs: - name: Run tests run: | python3 tests/test_version.py + PYTHONPATH=src python3 tests/test_output_shape.py + PYTHONPATH=src python3 tests/test_web_bundle.py python3 tests/test_parity.py build: diff --git a/.gitignore b/.gitignore index 8683278..815e97d 100644 --- a/.gitignore +++ b/.gitignore @@ -33,3 +33,5 @@ dist/ venv/ *.egg-info/ __pycache__/ + +act diff --git a/CHANGELOG.md b/CHANGELOG.md index f348e92..46d499d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,39 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.2.0] - 2026-04-11 + +### Added +- **Modern NVIDIA Container Support**: Support for `runtime: nvidia` in generated `docker-compose.yml` and the `__GLX_VENDOR_LIBRARY_NAME=nvidia` environment variable to ensure correct GPU offloading and OpenGL vendor selection. +- **Enhanced Hybrid GPU Support**: Native support for Intel/NVIDIA hybrid laptops via mandatory `/dev/dri` volume mounts for Linux hosts and explicit installation of `libgl1`, `libglvnd-dev`, and `libegl1` for vendor-neutral GL dispatch. +- **Intelligent Choice Relationships**: Variant selections like `desktop-full` now automatically imply required packages (`rviz2`, `gazebo`/`gz_sim`), and GUI packages automatically enable `x11` display forwarding. +- **Core Engine Upgrades**: New `resolve_config()` method in both Python and JS core modules to handle automatic dependency resolution and OS-specific scaffolding. +- Output-shape regression tests in `tests/test_output_shape.py` to verify repo-root workspace mounts, shell config generation, NVIDIA compose behavior, and README/package consistency. +- Web bundle regression tests in `tests/test_web_bundle.py` to verify the website zip export contains the expected files and preserves byte-identical generated content. +- Readiness checks in `CI/validate.sh` for writable default workspaces, `colcon mixin list`, and Oh My Zsh ownership in generated non-root containers. +- CI workflow coverage for the new readiness invariants, including mounted default workspace validation and source-change triggers for heavier generator test jobs. +- A lightweight zip bundler for the web UI and a local `scripts/run_act.sh` helper for reproducible sequential `act` runs without introducing third-party JS dependencies. + +### Changed +- Modernized GPU acceleration logic in `docker-compose.yml` to prefer the `runtime: nvidia` directive over the legacy `deploy: resources` block for broader compatibility outside of Swarm. +- Updated generated README with specific troubleshooting guidance and caveats for users on hybrid GPU laptop hardware (e.g., `prime-select nvidia` hints). +- Generated `docker-compose.yml` now mounts the current directory into the configured ROS workspace by default instead of creating a nested `./ros2_ws` bind mount. +- Generated Dockerfiles now install Oh My Zsh in the selected user's home after switching to that user, while keeping Zsh as a normal optional tool selection. +- Generated shell bootstrap now writes ROS/environment setup into `.bashrc` when `bashrc` is selected and into `.zshrc` when `zsh` is selected. +- The CLI wizard now preselects propagated package and tool defaults for presets such as `desktop-full`, matching the web UI instead of only applying those implications at final generation time. +- Generated README content now reflects the repo-root workspace workflow, includes clearer NVIDIA host/runtime guidance, and documents the new local `act` runner behavior. +- CLI/web generation and CI helpers now consistently use the standard non-root `user` mode and current host UID mapping for dev-container readiness. +- Shared defaults now drive Web, CLI, and CI generation consistently, including the default `ros2-` container name and default tool selection set. +- TensorRT now behaves like a real selectable package and emits install steps from the NVIDIA CUDA image package repositories instead of acting as a placeholder selection. +- The web UI now exports a real zip bundle containing `Dockerfile`, `docker-compose.yml`, and `README.md`, and local `act` runs now execute jobs sequentially with matrix fan-out capped to keep disk usage manageable. + +### Fixed +- Resolved generated non-root dev environments depending on Docker auto-creating a writable workspace directory with the correct ownership. +- Resolved generated Zsh environments installing Oh My Zsh under `/root` instead of the selected non-root user. +- Removed misleading generated SSH port documentation when host networking is enabled and Docker would discard published ports. +- Resolved website download bundles requiring manual renaming of `Dockerfile` before the generated compose setup could build. +- Resolved generated Gazebo environments missing the ROS plugin library path, which could prevent Gazebo from launching until `GAZEBO_PLUGIN_PATH` was exported manually. + ## [1.1.0] - 2026-03-25 ### Added diff --git a/CI/generate.js b/CI/generate.js index 26c59be..7700bec 100644 --- a/CI/generate.js +++ b/CI/generate.js @@ -18,18 +18,24 @@ function getArg(name, fallback = '') { const distro = getArg('distro', 'humble'); const variant = getArg('variant', 'ros-base'); const pkgArg = getArg('packages', ''); -const toolArg = getArg('tools', 'colcon,rosdep,python3,git,bashrc,locale,sudo'); -const username = getArg('username', 'ros-dev'); -const uid = parseInt(getArg('uid', '1000'), 10); const outDir = getArg('out', './ci-output'); -const cname = getArg('container', 'ros2_dev'); -const userType = getArg('usertype', 'user'); // Changed from 'custom' to match core.js expected values: 'user' | 'root' +const cname = getArg('container', ''); // ── Load Config & Init Core ─────────────────────────────────── const _ROOT = path.join(__dirname, '..'); const configPath = path.join(_ROOT, 'src', 'ros2_dockergen', 'data', 'config.json'); const configData = JSON.parse(fs.readFileSync(configPath, 'utf8')); CORE.init(configData); +const defaults = configData.defaults; +const defaultTools = Object.entries(configData.tools) + .filter(([, tool]) => tool.default) + .map(([key]) => key) + .join(','); +const toolArg = getArg('tools', defaultTools); +const rawUserType = getArg('usertype', defaults.user_type); +const userType = rawUserType === 'custom' ? 'user' : rawUserType; +const username = getArg('username', defaults.username); +const uid = parseInt(getArg('uid', String(defaults.uid)), 10); const config = { distro, @@ -39,8 +45,13 @@ const config = { username, uid, userType, - containerName: cname, - workspace: getArg('workspace', userType === 'root' ? '/root/ros2_ws' : `/home/${username}/ros2_ws`) + containerName: cname || CORE.defaultContainerName(distro), + workspace: getArg( + 'workspace', + userType === 'root' + ? defaults.root_workspace + : defaults.user_workspace.replace('{username}', username) + ) }; // ── Validate inputs ─────────────────────────────────────────── diff --git a/CI/validate.sh b/CI/validate.sh index d91f70a..acc049a 100644 --- a/CI/validate.sh +++ b/CI/validate.sh @@ -85,6 +85,69 @@ check_bashrc() { fi } +check_zshrc() { + local expect_zsh="${EXPECT_ZSH:-false}" + if [ "$expect_zsh" != "true" ]; then + skip "zsh not expected for this config" + return + fi + + section ".zshrc source line" + local user + user=$(whoami) + local zshrc + zshrc=$( [ "$user" = "root" ] && echo "/root/.zshrc" || echo "/home/${user}/.zshrc" ) + + if [ ! -f "$zshrc" ]; then + fail "$zshrc does not exist" + return + fi + + local count + count=$(grep -c "source /opt/ros/${DISTRO}/setup.bash" "$zshrc" || echo "0") + if [ "$count" -eq 1 ]; then + pass ".zshrc sources setup.bash exactly once" + elif [ "$count" -eq 0 ]; then + fail ".zshrc is missing setup.bash source line" + else + fail ".zshrc sources setup.bash ${count} times" + fi + + section "Oh My Zsh ownership" + local user_home + user_home=$( [ "$user" = "root" ] && echo "/root" || echo "/home/${user}" ) + if [ -d "${user_home}/.oh-my-zsh" ]; then + local owner + owner=$(stat -c '%U' "${user_home}/.oh-my-zsh") + if [ "$owner" = "$user" ]; then + pass "${user_home}/.oh-my-zsh exists and is owned by $user" + else + fail "${user_home}/.oh-my-zsh owned by $owner (expected: $user)" + fi + else + fail "${user_home}/.oh-my-zsh does not exist" + fi +} + +check_workdir_ready() { + section "Default workdir" + local expected_workspace="${EXPECTED_WORKSPACE:-$PWD}" + if [ "$PWD" = "$expected_workspace" ]; then + pass "current workdir = $PWD" + else + fail "current workdir = $PWD (expected: $expected_workspace)" + fi + + section "Workdir write access" + local probe=".ci-write-test" + if touch "$probe" 2>/dev/null; then + rm -f "$probe" + pass "default workdir is writable" + else + fail "default workdir is not writable" + fi +} + # ============================================================= # SUITE: base # ============================================================= @@ -92,6 +155,7 @@ run_base() { check_ros2_setup check_ros2_cli check_bashrc + check_workdir_ready section "ros2 topic list (basic DDS check)" source "/opt/ros/${DISTRO}/setup.bash" 2>/dev/null || true @@ -113,10 +177,17 @@ run_base() { run_build_tools() { check_ros2_setup check_ros2_cli + check_workdir_ready section "colcon" if command -v colcon &>/dev/null; then pass "colcon found: $(colcon --version 2>&1 | head -1)" + if colcon mixin list >/tmp/colcon-mixin.log 2>&1; then + pass "colcon mixin list works from default workdir" + else + fail "colcon mixin list failed from default workdir" + cat /tmp/colcon-mixin.log + fi else fail "colcon not found" fi @@ -183,7 +254,7 @@ check_colcon_build() { source "/opt/ros/${DISTRO}/setup.bash" 2>/dev/null || true local tmpdir - tmpdir=$(mktemp -d) + tmpdir=$(mktemp -d -p "$PWD" ci-colcon-XXXXXX) mkdir -p "$tmpdir/src/test_pkg" # Create a minimal package.xml and CMakeLists.txt @@ -260,7 +331,7 @@ run_user() { fi section "Workspace" - local ws="/home/${current_user}/ros2_ws" + local ws="${EXPECTED_WORKSPACE:-/home/${current_user}/ros2_ws}" if [ -d "$ws" ]; then local ws_owner ws_owner=$(stat -c '%U' "$ws") @@ -273,6 +344,8 @@ run_user() { fail "$ws not found" fi + check_workdir_ready + section "sudo" if [ "$expect_sudo" = "true" ]; then if sudo -n true 2>/dev/null; then @@ -285,9 +358,11 @@ run_user() { fi else pass "Running as root (root-mode build)" + check_workdir_ready fi check_bashrc + check_zshrc } # ============================================================= @@ -365,6 +440,7 @@ run_nvidia() { check_ros2_setup check_ros2_cli check_bashrc + check_workdir_ready } # ============================================================= diff --git a/README.md b/README.md index 6f48679..8a55574 100644 --- a/README.md +++ b/README.md @@ -58,6 +58,23 @@ The interactive wizard will walk you through 8 steps to configure your environme ros2-dockergen --help # Show help ros2-dockergen --version # Show version ``` +--- + +## Local CI With `act` + +To reproduce the main GitHub Actions workflow locally, install [`act`](https://nektosact.com/installation/) and run: + +```bash +./scripts/run_act.sh +``` + +By default this runs the CI jobs one-by-one so local Docker usage stays manageable and each job's output is easy to read. + +If you want the original full-workflow `act` behavior instead, run: + +```bash +./scripts/run_act.sh full +``` --- diff --git a/docs/hero.png b/docs/hero.png index f74a52b..5c3c072 100644 Binary files a/docs/hero.png and b/docs/hero.png differ diff --git a/index.html b/index.html index e16a0af..e91b1fc 100644 --- a/index.html +++ b/index.html @@ -4,11 +4,12 @@ - ROS2 Docker Generator v1.1.0 — Get Robotics Running Fast + ROS2 Docker Generator v1.2.0 — Get Robotics Running Fast +