Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .github/workflows/_test-user-setup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
16 changes: 14 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,5 @@ dist/
venv/
*.egg-info/
__pycache__/

act
33 changes: 33 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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-<distro>` 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
Expand Down
25 changes: 18 additions & 7 deletions CI/generate.js
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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 ───────────────────────────────────────────
Expand Down
80 changes: 78 additions & 2 deletions CI/validate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -85,13 +85,77 @@ 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
# =============================================================
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
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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")
Expand All @@ -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
Expand All @@ -285,9 +358,11 @@ run_user() {
fi
else
pass "Running as root (root-mode build)"
check_workdir_ready
fi

check_bashrc
check_zshrc
}

# =============================================================
Expand Down Expand Up @@ -365,6 +440,7 @@ run_nvidia() {
check_ros2_setup
check_ros2_cli
check_bashrc
check_workdir_ready
}

# =============================================================
Expand Down
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```

---

Expand Down
Binary file modified docs/hero.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading