diff --git a/src/commands/build.rs b/src/commands/build.rs index 48032bf..74da099 100644 --- a/src/commands/build.rs +++ b/src/commands/build.rs @@ -111,7 +111,10 @@ pub fn run_build( // Upload let port = match port { Some(p) => p.to_string(), - None => board::auto_detect_port()?, + None => match &config.monitor.port { + Some(p) => p.clone(), + None => board::auto_detect_port()?, + }, }; upload_to_board(&cli, fqbn, &port, &cache_dir, verbose)?; @@ -193,7 +196,10 @@ pub fn run_upload_only( let port = match port { Some(p) => p.to_string(), - None => board::auto_detect_port()?, + None => match &config.monitor.port { + Some(p) => p.clone(), + None => board::auto_detect_port()?, + }, }; upload_to_board(&cli, fqbn, &port, &cache_dir, verbose)?; diff --git a/src/project/config.rs b/src/project/config.rs index f1981eb..cb0614d 100644 --- a/src/project/config.rs +++ b/src/project/config.rs @@ -31,6 +31,8 @@ pub struct BuildConfig { #[derive(Debug, Serialize, Deserialize, Clone)] pub struct MonitorConfig { pub baud: u32, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub port: Option, } impl ProjectConfig { @@ -49,6 +51,7 @@ impl ProjectConfig { }, monitor: MonitorConfig { baud: 115200, + port: None, }, } } diff --git a/templates/basic/_dot_anvil.toml.tmpl b/templates/basic/_dot_anvil.toml.tmpl index c8afc54..b098e40 100644 --- a/templates/basic/_dot_anvil.toml.tmpl +++ b/templates/basic/_dot_anvil.toml.tmpl @@ -10,3 +10,4 @@ extra_flags = ["-Werror"] [monitor] baud = 115200 +# port = "/dev/ttyUSB0" # Uncomment to skip auto-detect diff --git a/templates/basic/build.sh b/templates/basic/build.sh index f0bbc0a..e796bd5 100644 --- a/templates/basic/build.sh +++ b/templates/basic/build.sh @@ -36,12 +36,12 @@ die() { echo "${RED}FAIL${RST} $*" >&2; exit 1; } # Searches the whole file; for sectioned keys, grep is specific enough # given our small, flat schema. toml_get() { - grep "^$1 " "$CONFIG" | head -1 | sed 's/.*= *"\{0,1\}\([^"]*\)"\{0,1\}/\1/' | tr -d ' ' + (grep "^$1 " "$CONFIG" 2>/dev/null || true) | head -1 | sed 's/.*= *"\{0,1\}\([^"]*\)"\{0,1\}/\1/' | tr -d ' ' } # Extract a TOML array as space-separated values: toml_array "key" toml_array() { - grep "^$1 " "$CONFIG" | head -1 \ + (grep "^$1 " "$CONFIG" 2>/dev/null || true) | head -1 \ | sed 's/.*\[//; s/\].*//; s/"//g; s/,/ /g' | tr -s ' ' } diff --git a/templates/basic/monitor.sh b/templates/basic/monitor.sh index 2e037ee..abf412e 100644 --- a/templates/basic/monitor.sh +++ b/templates/basic/monitor.sh @@ -32,11 +32,12 @@ die() { echo "${RED}FAIL${RST} $*" >&2; exit 1; } [[ -f "$CONFIG" ]] || die "No .anvil.toml found in $SCRIPT_DIR" toml_get() { - grep "^$1 " "$CONFIG" | head -1 | sed 's/.*= *"\{0,1\}\([^"]*\)"\{0,1\}/\1/' | tr -d ' ' + (grep "^$1 " "$CONFIG" 2>/dev/null || true) | head -1 | sed 's/.*= *"\{0,1\}\([^"]*\)"\{0,1\}/\1/' | tr -d ' ' } BAUD="$(toml_get 'baud')" BAUD="${BAUD:-115200}" +DEFAULT_PORT="$(toml_get 'port')" # -- Parse arguments ------------------------------------------------------- PORT="" @@ -62,16 +63,35 @@ command -v arduino-cli &>/dev/null \ # -- Auto-detect port ------------------------------------------------------ auto_detect() { - arduino-cli board list 2>/dev/null \ + # Prefer ttyUSB/ttyACM (real USB devices) over ttyS (hardware UART) + local port + port=$(arduino-cli board list 2>/dev/null \ | grep -i "serial" \ - | head -1 \ - | awk '{print $1}' + | awk '{print $1}' \ + | grep -E 'ttyUSB|ttyACM|COM' \ + | head -1) + + # Fallback: any serial port + if [[ -z "$port" ]]; then + port=$(arduino-cli board list 2>/dev/null \ + | grep -i "serial" \ + | head -1 \ + | awk '{print $1}') + fi + + echo "$port" } if [[ -z "$PORT" ]]; then - PORT="$(auto_detect)" + # Check .anvil.toml for configured port + if [[ -n "$DEFAULT_PORT" ]]; then + PORT="$DEFAULT_PORT" + else + PORT="$(auto_detect)" + fi + if [[ -z "$PORT" ]]; then - die "No serial port detected. Is the board plugged in?\n Specify manually: ./monitor.sh -p /dev/ttyUSB0" + die "No serial port detected. Is the board plugged in?\n Specify manually: ./monitor.sh -p /dev/ttyUSB0\n Or set port in .anvil.toml" fi warn "Auto-detected port: $PORT (use -p to override)" fi diff --git a/templates/basic/upload.sh b/templates/basic/upload.sh index f0d8644..883f860 100644 --- a/templates/basic/upload.sh +++ b/templates/basic/upload.sh @@ -34,11 +34,11 @@ die() { echo "${RED}FAIL${RST} $*" >&2; exit 1; } [[ -f "$CONFIG" ]] || die "No .anvil.toml found in $SCRIPT_DIR" toml_get() { - grep "^$1 " "$CONFIG" | head -1 | sed 's/.*= *"\{0,1\}\([^"]*\)"\{0,1\}/\1/' | tr -d ' ' + (grep "^$1 " "$CONFIG" 2>/dev/null || true) | head -1 | sed 's/.*= *"\{0,1\}\([^"]*\)"\{0,1\}/\1/' | tr -d ' ' } toml_array() { - grep "^$1 " "$CONFIG" | head -1 \ + (grep "^$1 " "$CONFIG" 2>/dev/null || true) | head -1 \ | sed 's/.*\[//; s/\].*//; s/"//g; s/,/ /g' | tr -s ' ' } @@ -53,6 +53,7 @@ BAUD="$(toml_get 'baud')" [[ -n "$FQBN" ]] || die "Could not read fqbn from .anvil.toml" BAUD="${BAUD:-115200}" +DEFAULT_PORT="$(toml_get 'port')" SKETCH_DIR="$SCRIPT_DIR/$SKETCH_NAME" BUILD_DIR="$SCRIPT_DIR/.build" @@ -86,14 +87,28 @@ command -v arduino-cli &>/dev/null \ # -- Auto-detect port ------------------------------------------------------ if [[ -z "$PORT" ]]; then - # Look for the first serial port arduino-cli can see - PORT=$(arduino-cli board list 2>/dev/null \ - | grep -i "serial" \ - | head -1 \ - | awk '{print $1}') + # Check .anvil.toml for configured port + if [[ -n "$DEFAULT_PORT" ]]; then + PORT="$DEFAULT_PORT" + else + # Prefer ttyUSB/ttyACM (real USB devices) over ttyS (hardware UART) + PORT=$(arduino-cli board list 2>/dev/null \ + | grep -i "serial" \ + | awk '{print $1}' \ + | grep -E 'ttyUSB|ttyACM|COM' \ + | head -1) + + # Fallback: any serial port if no USB ports found + if [[ -z "$PORT" ]]; then + PORT=$(arduino-cli board list 2>/dev/null \ + | grep -i "serial" \ + | head -1 \ + | awk '{print $1}') + fi + fi if [[ -z "$PORT" ]]; then - die "No serial port detected. Is the board plugged in?\n Specify manually: ./upload.sh -p /dev/ttyUSB0" + die "No serial port detected. Is the board plugged in?\n Specify manually: ./upload.sh -p /dev/ttyUSB0\n Or set port in .anvil.toml" fi warn "Auto-detected port: $PORT (use -p to override)" diff --git a/tests/integration_test.rs b/tests/integration_test.rs index fbd07d1..96d5979 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -637,3 +637,35 @@ fn test_readme_documents_self_contained_workflow() { "README should mention self-contained" ); } + +#[test] +fn test_scripts_tolerate_missing_toml_keys() { + // Regression: toml_get must not kill the script when a key is absent. + // With set -euo pipefail, bare grep returns exit 1 on no match, + // pipefail propagates it, and set -e terminates silently. + // Every grep in toml_get/toml_array must have "|| true". + let tmp = TempDir::new().unwrap(); + let ctx = TemplateContext { + project_name: "grep_safe".to_string(), + anvil_version: "1.0.0".to_string(), + }; + + TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); + + for script in &["build.sh", "upload.sh", "monitor.sh"] { + let content = fs::read_to_string(tmp.path().join(script)).unwrap(); + + // If the script uses set -e (or -euo pipefail), then every + // toml_get/toml_array function must guard grep with || true + if content.contains("set -e") || content.contains("set -euo") { + // Find the toml_get function body and check for || true + let has_safe_grep = content.contains("|| true"); + assert!( + has_safe_grep, + "{} uses set -e but toml_get/toml_array lacks '|| true' guard. \ + Missing TOML keys will silently kill the script.", + script + ); + } + } +}