diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index b7c50370..ec07a83b 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -5,7 +5,7 @@ on: paths: - 'Dockerfile' - '.github/workflows/docker-publish.yml' - branches: [ "main" ] + branches: [ "master" ] tags: [ 'docker-v*' ] workflow_dispatch: @@ -56,6 +56,8 @@ jobs: context: . platforms: linux/amd64 push: ${{ github.event_name != 'pull_request' }} + build-args: | + CACHEBUST=${{ github.run_id }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha diff --git a/Cargo.lock b/Cargo.lock index 9ecee2e2..4f30de22 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2282,7 +2282,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "verilib-cli" -version = "0.1.9" +version = "0.2.0-beta.1" dependencies = [ "anyhow", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 8d7335ab..9080bea9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "verilib-cli" -version = "0.1.9" +version = "0.2.0-beta.1" edition = "2021" description = "A command-line tool for managing Verilib repositories and API interactions" homepage = "https://github.com/Beneficial-AI-Foundation/verilib-cli" diff --git a/Dockerfile b/Dockerfile index 192c81c3..32655d6b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -26,11 +26,18 @@ RUN echo "n" | python3 verus_analyzer_installer.py RUN echo "n" | python3 scip_installer.py WORKDIR /build +ARG CACHEBUST=1 RUN git clone https://github.com/Beneficial-AI-Foundation/probe-verus.git WORKDIR /build/probe-verus RUN cargo install --path . +# Download scripts +WORKDIR /build +RUN git clone https://github.com/Beneficial-AI-Foundation/dalek-lite.git /build/dalek-lite && \ + cd /build/dalek-lite && \ + git checkout c36b395aa5526af7940d1db0f66ea60db4e3a157 + FROM --platform=linux/amd64 rust:slim-bookworm AS runtime RUN apt-get update && apt-get install -y \ @@ -41,6 +48,9 @@ RUN apt-get update && apt-get install -y \ python3-requests \ && rm -rf /var/lib/apt/lists/* +# Install uv python package manager +RUN pip3 install uv --break-system-packages + # Install specific Rust toolchain for Verus RUN rustup toolchain install 1.93.0 @@ -64,8 +74,22 @@ ENV PATH="/usr/local/verus-analyzer:${PATH}" COPY --from=builder /root/scip /usr/local/scip ENV PATH="/usr/local/scip:${PATH}" +# Copy scripts from builder +COPY --from=builder /build/dalek-lite/scripts /usr/local/bin/scripts +ENV PATH="/usr/local/bin/scripts:${PATH}" + +# Add local analysis script override +COPY scripts/analyze_verus_specs_proofs.py /usr/local/bin/scripts/analyze_verus_specs_proofs.py + +# Patch script to allow REPO_ROOT env var and generic crate name +RUN sed -i 's|repo_root = script_dir.parent|import os; repo_root = Path(os.environ.get("REPO_ROOT", script_dir.parent))|' /usr/local/bin/scripts/analyze_verus_specs_proofs.py && \ + sed -i 's|CRATE_NAME = "curve25519_dalek"|import os; CRATE_NAME = os.environ.get("CRATE_NAME", "curve25519_dalek")|' /usr/local/bin/scripts/analyze_verus_specs_proofs.py && \ + sed -i 's|CRATE_DIR = "curve25519-dalek"|CRATE_DIR = os.environ.get("CRATE_DIR", "curve25519-dalek")|' /usr/local/bin/scripts/analyze_verus_specs_proofs.py && \ + sed -i 's|skip_parts = {"curve25519-dalek", "src"}|skip_parts = {CRATE_DIR, "src"}|' /usr/local/bin/scripts/analyze_verus_specs_proofs.py && \ + sed -i 's|module_stripped = module.replace(f"{CRATE_NAME}::", "")|module_stripped = module.replace(f"{CRATE_NAME}::", "")|' /usr/local/bin/scripts/analyze_verus_specs_proofs.py + # Ensure permissions for copied tools -RUN chmod -R a+rx /usr/local/verus /usr/local/verus-analyzer /usr/local/scip +RUN chmod -R a+rx /usr/local/verus /usr/local/verus-analyzer /usr/local/scip /usr/local/bin/scripts # Setup workspace WORKDIR /workspace diff --git a/README.md b/README.md index 8647f1a0..1b2d1e8c 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,25 @@ A command-line tool for managing Verilib repositories, verification structure fi 1. **Docker (Recommended)**: Runs `probe-verus` in a container with all dependencies pre-installed. This ensures a consistent environment and avoids local setup issues. You simply need Docker installed and running. 2. **Local**: Runs `probe-verus` directly on your host machine. This requires you to install `probe-verus` and all its dependencies (Rust, Verus, etc.) manually. +### Using a Local Docker Image + +If you want to use a locally built Docker image instead of pulling from the registry (e.g., for development or custom modifications), follow these steps: + +1. **Build the Docker image locally:** + ```bash + docker build --build-arg CACHEBUST=$(date +%s) -t verilib-cli:local . + ``` + +2. **Update your configuration:** + Edit `.verilib/config.json` in your project root and set the `docker-image` field: + ```json + { + "execution-mode": "docker", + "docker-image": "verilib-cli:local", + ... + } + ``` + During initialization (`verilib-cli init`), you will be prompted to choose your preferred execution mode. You can also change it later by editing the `.verilib/config.json` file. > **Note for Local Mode:** If you choose to run locally and encounter issues with missing dependencies or environment configuration, please refer to the [probe-verus repository](https://github.com/Beneficial-AI-Foundation/probe-verus) for installation instructions and troubleshooting. @@ -214,11 +233,9 @@ verilib-cli create --root custom/path |--------|-------------| | `--root ` | Custom structure root (default: `.verilib/structure`) | -**Requirements:** -- `scripts/analyze_verus_specs_proofs.py` script - **Optional:** -- `functions_to_track.csv` in project root — when absent, a minimal seed is used +- `scripts/analyze_verus_specs_proofs.py` — when present (in project or bundled), generates structure files; when absent, create still runs and sets up config with no structure files +- `functions_to_track.csv` in project root — when absent, all functions are tracked by default (when script runs) ### `atomize` Enrich structure files with metadata from SCIP atoms. diff --git a/functions_to_track.csv b/functions_to_track.csv new file mode 100644 index 00000000..9c40a421 --- /dev/null +++ b/functions_to_track.csv @@ -0,0 +1,136 @@ +function,module,impl_block +parse,verilib-cli::structure::frontmatter, +FileStorage::delete_password,verilib-cli::storage::file, +create_repo_from_git_url,verilib-cli::commands::init, +handle_create,verilib-cli::commands::create, +save_config_from_response,verilib-cli::commands::deploy, +fetch_verifier_versions,verilib-cli::commands::deploy, +load_proofs_from_file,verilib-cli::commands::verify, +run_analyze_verus_specs_proofs,verilib-cli::commands::create, +cleanup_intermediate_files,verilib-cli::structure::probe, +run_command,verilib-cli::structure::utils, +generate_stubs,verilib-cli::commands::atomize, +FileStorage::set_password,verilib-cli::storage::file, +generate_probe_atoms,verilib-cli::commands::atomize, +handle_list,verilib-cli::commands::api, +get_credential_storage,verilib-cli::storage, +StructureConfig::save,verilib-cli::structure::config, +get_display_name,verilib-cli::structure::utils, +update_structure_files,verilib-cli::commands::atomize, +disambiguate_names,verilib-cli::commands::create, +collect_certifications,verilib-cli::commands::specify, +test_specify_check_only_reports_uncertified,verilib-cli::specify_tests, +handle_batch,verilib-cli::commands::api, +run_local,verilib-cli::structure::executor, +require_probe_installed,verilib-cli::structure::probe, +test_atomize_check_only_passes_when_stubs_match,verilib-cli::atomize_tests, +enrich_stubs,verilib-cli::commands::atomize, +write,verilib-cli::structure::frontmatter, +check_stubs_match,verilib-cli::commands::atomize, +check_admin_status,verilib-cli::commands::api, +handle_api_error,verilib-cli::download::error, +resolve_code_name_and_atom,verilib-cli::commands::atomize, +load_atoms_from_file,verilib-cli::commands::atomize, +print_platform_help,verilib-cli::storage, +test_specify_no_probe_loads_specs_from_file,verilib-cli::specify_tests, +test_commands_create_default_config_when_missing,verilib-cli::error_handling_tests, +test_atomize_update_stubs_updates_md_files,verilib-cli::atomize_tests, +tracked_to_structure,verilib-cli::commands::create, +wait_for_atomization,verilib-cli::download::client, +build_enriched_entry,verilib-cli::commands::atomize, +is_git_available,verilib-cli::commands::reclone, +get_platform_info,verilib-cli::storage, +run_command,verilib-cli::structure::executor, +ConfigPaths::load,verilib-cli::structure::config, +FileStorage::ensure_secure_file,verilib-cli::storage::file, +StructureConfig::new,verilib-cli::structure::config, +test_api_key_validation,verilib-cli::tests, +read_repo_id_from_config,verilib-cli::commands::deploy, +run_docker,verilib-cli::structure::executor, +update_stubs_with_verification,verilib-cli::commands::verify, +incorporate_spec_text,verilib-cli::commands::specify, +default_docker_image,verilib-cli::structure::executor, +test_verify_updates_stubs_with_verification_status,verilib-cli::verify_tests, +prompt_type,verilib-cli::commands::deploy, +encode_name,verilib-cli::structure::certs, +StorageType::should_use_file_storage,verilib-cli::storage::types, +handle_reclone,verilib-cli::commands::reclone, +validate_meta_file,verilib-cli::commands::api, +write_stubs_json,verilib-cli::commands::specify, +prompt_execution_mode,verilib-cli::commands::init, +handle_set,verilib-cli::commands::api, +init_required_msg,verilib-cli::constants, +generate_structure_files,verilib-cli::commands::create, +has_uncommitted_changes,verilib-cli::commands::reclone, +test_verify_fails_without_stubs_json,verilib-cli::error_handling_tests, +test_specify_check_only_passes_when_all_certified,verilib-cli::specify_tests, +run_probe_specify,verilib-cli::commands::specify, +StorageType::from_env,verilib-cli::storage::types, +test_verify_check_only_detects_failures,verilib-cli::verify_tests, +parse_github_link,verilib-cli::structure::utils, +test_verify_no_probe_loads_proofs_from_file,verilib-cli::verify_tests, +handle_api,verilib-cli::commands::api, +test_atomize_no_probe_loads_atoms_from_file,verilib-cli::atomize_tests, +check_all_certified,verilib-cli::commands::specify, +handle_init,verilib-cli::commands::init, +prompt_language,verilib-cli::commands::deploy, +handle_get,verilib-cli::commands::api, +save_config,verilib-cli::commands::init, +update_stubs_specification_status,verilib-cli::commands::specify, +CredentialStorageFactory::create,verilib-cli::CredentialStorageFactory bool { + walkdir::WalkDir::new(structure_root) + .into_iter() + .filter_map(|e| e.ok()) + .any(|e| e.path().extension().map_or(false, |ext| ext == "md")) +} + /// Run probe-verus stubify to generate stubs.json from .md files. fn generate_stubs( project_root: &Path, @@ -79,11 +87,20 @@ fn generate_stubs( stubs_path: &Path, config: &CommandConfig, ) -> Result> { - require_probe_installed(config)?; - if let Some(parent) = stubs_path.parent() { std::fs::create_dir_all(parent)?; } + // Ensure structure root exists (create may have written 0 files) + std::fs::create_dir_all(structure_root)?; + + // No .md files: stubify would fail; write empty stubs.json and continue + if !has_md_files(structure_root) { + println!("No .md structure files found; writing empty stubs.json"); + std::fs::write(stubs_path, "{}")?; + return Ok(HashMap::new()); + } + + require_probe_installed(config)?; println!( "Running probe-verus stubify on {}...", diff --git a/src/commands/create.rs b/src/commands/create.rs index 8799c5be..5c4c10b2 100644 --- a/src/commands/create.rs +++ b/src/commands/create.rs @@ -3,7 +3,7 @@ //! Initialize structure files from source analysis. use crate::structure::{ - parse_github_link, run_command, write_frontmatter, CommandConfig, ExecutionMode, StructureConfig, + parse_github_link, run_command, write_frontmatter, CommandConfig, ConfigPaths, ExecutionMode, StructureConfig, }; use anyhow::{bail, Context, Result}; use serde_json::{json, Value}; @@ -24,30 +24,35 @@ pub async fn handle_create(project_root: PathBuf, root: Option) -> Resu // Write config file with ONLY structure-root field let config = StructureConfig::new(&structure_root_relative); - let config_path = config.save(&project_root)?; + let config_path = config.save(&project_root, true)?; println!("Wrote config to {}", config_path.display()); // NOTE: .gitignore creation is moved to the 'init' subcommand + let config = ConfigPaths::load(&project_root)?; + let tracked_path = project_root.join("functions_to_track.csv"); - let seed_path: PathBuf = if tracked_path.exists() { - tracked_path - } else { - // Optional: use minimal seed when functions_to_track.csv is absent - let fallback_seed = verilib_path.join("seed.csv"); - std::fs::write(&fallback_seed, "function,module,link\n") - .context("Failed to write fallback seed.csv")?; - fallback_seed - }; + if !tracked_path.exists() { + println!("functions_to_track.csv not found, generating from atomize..."); + crate::commands::atomize::handle_atomize( + project_root.clone(), + false, + false, + false, + ) + .await?; + + let atoms_path = verilib_path.join("atoms.json"); + if atoms_path.exists() { + generate_functions_to_track_csv(&atoms_path, &tracked_path)?; + } else { + bail!("Failed to generate atoms.json for functions_to_track.csv"); + } + } let tracked_output_path = verilib_path.join("tracked_functions.csv"); - // This command uses 'uv' which we expect to be local. - let local_config = CommandConfig { - execution_mode: ExecutionMode::Local, - ..Default::default() - }; - run_analyze_verus_specs_proofs(&project_root, &seed_path, &tracked_output_path, &local_config)?; + run_analyze_verus_specs_proofs(&project_root, &tracked_path, &tracked_output_path, &config.command_config)?; let tracked = read_tracked_csv(&tracked_output_path)?; let tracked = disambiguate_names(tracked); @@ -68,44 +73,76 @@ fn run_analyze_verus_specs_proofs( output_path: &Path, config: &CommandConfig, ) -> Result<()> { - let script_path = project_root - .join("scripts") - .join("analyze_verus_specs_proofs.py"); - if !script_path.exists() { - bail!("Script not found: {}", script_path.display()); - } + let script_name = "analyze_verus_specs_proofs.py"; + let script_path = if matches!(config.execution_mode, ExecutionMode::Docker) { + let workspace_script = PathBuf::from("/workspace/scripts").join(script_name); + + if project_root.join("scripts").join(script_name).exists() { + workspace_script + } else { + PathBuf::from("/usr/local/bin/scripts").join(script_name) + } + } else { + let path = project_root.join("scripts").join(script_name); + if !path.exists() { + bail!("Script not found locally: {}", path.display()); + } + path + }; - println!("Running analyze_verus_specs_proofs.py..."); + println!("Running {}...", script_name); - let seed_relative = seed_path.strip_prefix(project_root).unwrap_or(seed_path); - let output_relative = output_path - .strip_prefix(project_root) - .unwrap_or(output_path); + let seed_arg = if matches!(config.execution_mode, ExecutionMode::Docker) { + seed_path.strip_prefix(project_root).unwrap_or(seed_path).to_string_lossy().to_string() + } else { + seed_path.to_string_lossy().to_string() + }; + + let output_arg = if matches!(config.execution_mode, ExecutionMode::Docker) { + output_path.strip_prefix(project_root).unwrap_or(output_path).to_string_lossy().to_string() + } else { + output_path.to_string_lossy().to_string() + }; - // Ensure parent directory exists + // Ensure parent directory exists (locally) if let Some(parent) = output_path.parent() { std::fs::create_dir_all(parent)?; } + + let (seed_flag, output_flag) = if matches!(config.execution_mode, ExecutionMode::Docker) { + ( + format!("/workspace/{}", seed_arg), + format!("/workspace/{}", output_arg), + ) + } else { + (seed_arg.clone(), output_arg.clone()) + }; + let script_path_str = script_path.to_string_lossy(); + let args = vec![ + "run", + &script_path_str, + "--seed", + &seed_flag, + "--output", + &output_flag, + ]; + let output = run_command( "uv", - &[ - "run", - script_path.to_str().unwrap(), - "--seed", - seed_relative.to_str().unwrap(), - "--output", - output_relative.to_str().unwrap(), - ], + &args, Some(project_root), config, )?; if !output.status.success() { let stderr = String::from_utf8_lossy(&output.stderr); - eprintln!("Error running analyze_verus_specs_proofs.py:\n{}", stderr); - bail!("analyze_verus_specs_proofs.py failed"); + eprintln!("Error running {}:\n{}", script_name, stderr); + bail!("{} failed", script_name); } + + let stdout = String::from_utf8_lossy(&output.stdout); + println!("{} output:\n{}", script_name, stdout); println!( "Generated tracked functions CSV at {}", @@ -114,6 +151,58 @@ fn run_analyze_verus_specs_proofs( Ok(()) } +/// Generate functions_to_track.csv from atoms.json +fn generate_functions_to_track_csv(atoms_path: &Path, output_path: &Path) -> Result<()> { + let file = std::fs::File::open(atoms_path)?; + let reader = std::io::BufReader::new(file); + let atoms: HashMap = serde_json::from_reader(reader)?; + + let mut wtr = csv::Writer::from_path(output_path)?; + wtr.write_record(&["function", "module", "impl_block"])?; + + for (key, val) in atoms { + if !key.starts_with("probe:") { + continue; + } + + let parts: Vec<&str> = key.split('/').collect(); + if parts.len() < 3 { continue; } + + let project_part = parts[0]; + let project_name = project_part.strip_prefix("probe:").unwrap_or(project_part); + + let func_part = parts.last().unwrap(); + + let function = val.get("display-name") + .and_then(|v| v.as_str()) + .unwrap_or(func_part) + .to_string() + "()"; + + if parts.len() <= 2 { + continue; + } + + let dir_parts = &parts[2..parts.len()-1]; + if dir_parts.is_empty() { + continue; + } + + let mut rev_parts: Vec<&str> = dir_parts.to_vec(); + rev_parts.reverse(); + + let mut module_parts = vec![project_name]; + module_parts.extend(rev_parts); + let module = module_parts.join("::"); + + wtr.write_record(&[&function, &module, ""])?; + } + + wtr.flush()?; + println!("Generated functions_to_track.csv at {}", output_path.display()); + Ok(()) +} + + /// Tracked function data from CSV. #[derive(Debug, Clone)] struct TrackedFunction { @@ -243,4 +332,4 @@ fn generate_structure_files( structure_root.display() ); Ok(()) -} +} \ No newline at end of file diff --git a/src/commands/init.rs b/src/commands/init.rs index f522df1d..57111a02 100644 --- a/src/commands/init.rs +++ b/src/commands/init.rs @@ -263,7 +263,7 @@ fn save_config( fs::write(&config_path, &config_json).context("Failed to write config.json file")?; // Create .gitignore for generated files - create_gitignore(&verilib_path)?; + // create_gitignore(&verilib_path)?; Ok(()) } diff --git a/src/structure/config.rs b/src/structure/config.rs index c1c5d3bc..a0aec497 100644 --- a/src/structure/config.rs +++ b/src/structure/config.rs @@ -46,15 +46,13 @@ impl StructureConfig { } } - /// Save config to .verilib/config.json - pub fn save(&self, project_root: &Path) -> Result { + /// Save config to .verilib/config.json. + pub fn save(&self, project_root: &Path, preserve_existing: bool) -> Result { let verilib_path = project_root.join(".verilib"); std::fs::create_dir_all(&verilib_path).context("Failed to create .verilib directory")?; let config_path = verilib_path.join("config.json"); - // If config exists, merge with existing content - // We read it into a Value to preserve other fields, but we should also respect our own fields. let mut json: serde_json::Value = if config_path.exists() { let existing = std::fs::read_to_string(&config_path) .context("Failed to read existing config.json")?; @@ -63,11 +61,20 @@ impl StructureConfig { serde_json::json!({}) }; - // Update fields + // Always update structure-root as that's the primary purpose of this save in 'create' json["structure-root"] = serde_json::Value::String(self.structure_root.clone()); - json["execution-mode"] = serde_json::to_value(&self.execution_mode).unwrap_or(serde_json::Value::Null); - json["docker-image"] = serde_json::Value::String(self.docker_image.clone()); - json["auto-validate-specs"] = serde_json::Value::Bool(self.auto_validate_specs); + + if !preserve_existing || json.get("execution-mode").is_none() { + json["execution-mode"] = serde_json::to_value(&self.execution_mode).unwrap_or(serde_json::Value::Null); + } + + if !preserve_existing || json.get("docker-image").is_none() { + json["docker-image"] = serde_json::Value::String(self.docker_image.clone()); + } + + if !preserve_existing || (json.get("auto-validate-specs").is_none() && json.get("auto_validate_specs").is_none()) { + json["auto-validate-specs"] = serde_json::Value::Bool(self.auto_validate_specs); + } let content = serde_json::to_string_pretty(&json).context("Failed to serialize config")?; std::fs::write(&config_path, content).context("Failed to write config.json")?; @@ -89,18 +96,27 @@ pub fn create_gitignore(verilib_path: &Path) -> Result<()> { Ok(()) } +/// Default config written when .verilib/config.json is missing. +const DEFAULT_CONFIG_JSON: &str = r#"{ + "docker-image": "ghcr.io/beneficial-ai-foundation/verilib-cli:latest", + "execution-mode": "local", + "repo": {}, + "structure-root": ".verilib/structure" +}"#; + impl ConfigPaths { /// Load config and compute all paths. - /// Requires structure-root to be present in config. + /// If config.json is missing, creates a default one and continues. pub fn load(project_root: &Path) -> Result { let verilib_path = project_root.join(".verilib"); let config_path = verilib_path.join("config.json"); if !config_path.exists() { - anyhow::bail!( - "{} not found. Run 'verilib-cli create' first.", - config_path.display() - ); + std::fs::create_dir_all(&verilib_path) + .context("Failed to create .verilib directory")?; + std::fs::write(&config_path, DEFAULT_CONFIG_JSON) + .context("Failed to write default config.json")?; + println!("Created default config at {}", config_path.display()); } let content = @@ -112,11 +128,7 @@ impl ConfigPaths { let structure_root_str = json .get("structure-root") .and_then(|v| v.as_str()) - .ok_or_else(|| { - anyhow::anyhow!( - "No 'structure-root' field in config.json. Run 'verilib-cli create' first." - ) - })?; + .unwrap_or(".verilib/structure"); let structure_root = project_root.join(structure_root_str); diff --git a/src/structure/executor.rs b/src/structure/executor.rs index 76b06b1f..49373c23 100644 --- a/src/structure/executor.rs +++ b/src/structure/executor.rs @@ -64,33 +64,6 @@ fn run_local(program: &str, args: &[&str], cwd: Option<&Path>) -> Result Ok(output) } -fn ensure_image_pulled(image: &str) -> Result<()> { - let status = Command::new("docker") - .args(&["image", "inspect", image]) - .stdout(std::process::Stdio::null()) - .stderr(std::process::Stdio::null()) - .status(); - - if let Ok(status) = status { - if status.success() { - return Ok(()); - } - } - - println!("Docker image {} not found locally. Pulling...", image); - - let status = Command::new("docker") - .args(&["pull", "--platform", "linux/amd64", image]) - .status() - .context(format!("Failed to pull docker image {}", image))?; - - if !status.success() { - anyhow::bail!("Failed to pull docker image {}", image); - } - - Ok(()) -} - fn run_docker( program: &str, args: &[&str], @@ -119,9 +92,20 @@ fn run_docker( let mut docker_args = vec![ "run", "--rm", - "--platform", "linux/amd64", - "--entrypoint", program, - "-u", &user_arg, + "--entrypoint", + program, + "-u", + &user_arg, + "-e", + "HOME=/tmp", + "-e", + "XDG_CACHE_HOME=/tmp/.cache", + "-e", + "UV_CACHE_DIR=/tmp/.uv-cache", + "-e", + "UV_PYTHON_INSTALL_DIR=/tmp/.python", + "-e", + "REPO_ROOT=/workspace", "-v", ]; @@ -129,8 +113,7 @@ fn run_docker( docker_args.push(&mount_arg); docker_args.extend_from_slice(&[ - "--tmpfs", "/tmp", - "--tmpfs", "/home/tooluser/.cache", + "--tmpfs", "/tmp:exec,mode=777", "--security-opt=no-new-privileges", "-w", "/workspace", image, @@ -145,3 +128,32 @@ fn run_docker( Ok(output) } + +fn ensure_image_pulled(image: &str) -> Result<()> { + // 1. Check if image exists locally (e.g. built locally) + let inspect = Command::new("docker") + .args(&["image", "inspect", image]) + .output(); + + if let Ok(output) = inspect { + if output.status.success() { + // Found locally, no need to pull + return Ok(()); + } + } + + // 2. Not found locally, try to pull from registry + println!("Docker image '{}' not found locally. Pulling...", image); + + let status = Command::new("docker") + .args(&["pull", "--platform", "linux/amd64", image]) + .status() + .context(format!("Failed to pull docker image {}", image))?; + + if !status.success() { + // We can't proceed if we don't have the image + anyhow::bail!("Failed to pull docker image {} (and not found locally)", image); + } + + Ok(()) +} diff --git a/tests/structure_commands_test.rs b/tests/structure_commands_test.rs index 1cfd7af8..eeb34cae 100644 --- a/tests/structure_commands_test.rs +++ b/tests/structure_commands_test.rs @@ -424,10 +424,10 @@ mod create_tests { use super::*; #[test] - fn test_create_uses_fallback_seed_without_functions_to_track_csv() { + fn test_create_tracks_all_functions_without_functions_to_track_csv() { let temp_dir = TempDir::new().expect("Failed to create temp dir"); - // No functions_to_track.csv - create uses fallback seed and does not fail for that reason + // No functions_to_track.csv - create invokes script without --seed (tracks all functions) let output = run_command(&["create"], temp_dir.path()); let stderr = String::from_utf8_lossy(&output.stderr); @@ -436,12 +436,6 @@ mod create_tests { "Should NOT fail with 'functions_to_track.csv not found' (now optional): {}", stderr ); - // Fallback seed should be created when functions_to_track.csv is absent - let seed_path = temp_dir.path().join(".verilib").join("seed.csv"); - assert!( - seed_path.exists(), - "Fallback .verilib/seed.csv should be created when functions_to_track.csv is missing" - ); } #[test] @@ -481,18 +475,17 @@ mod error_handling_tests { use super::*; #[test] - fn test_commands_fail_without_config() { + fn test_commands_create_default_config_when_missing() { let temp_dir = TempDir::new().expect("Failed to create temp dir"); let verilib_dir = temp_dir.path().join(".verilib"); - fs::create_dir_all(&verilib_dir).expect("Failed to create .verilib dir"); - // No config.json - should fail - let output = run_command(&["atomize", "--no-probe"], temp_dir.path()); - assert!(!output.status.success()); - let stderr = String::from_utf8_lossy(&output.stderr); + // No .verilib at all - atomize creates default config and proceeds + run_command(&["atomize", "--no-probe"], temp_dir.path()); + // config.json should have been created (fails later on atoms.json) + let config_path = verilib_dir.join("config.json"); assert!( - stderr.contains("config.json not found") || stderr.contains("Run 'verilib-cli create'"), - "Should report missing config" + config_path.exists(), + "Default config.json should be created when missing" ); }