diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml index e197d3de..d2f6bc32 100644 --- a/.github/workflows/reusable-build.yml +++ b/.github/workflows/reusable-build.yml @@ -21,7 +21,7 @@ on: jobs: build: - name: Build and test infc + name: Build and test strategy: matrix: os: [ubuntu-latest, windows-latest, macos-14] @@ -154,7 +154,6 @@ jobs: run: | New-Item -ItemType Directory -Force -Path artifact-infc Copy-Item target\x86_64-pc-windows-gnu\release\infc.exe artifact-infc\ - Copy-Item book\check_deps.ps1 artifact-infc\ - name: Prepare infs Artifact Package (Windows) if: runner.os == 'Windows' && inputs.package-artifacts && inputs.release-build diff --git a/CHANGELOG.md b/CHANGELOG.md index a77252d1..0326bd47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -63,7 +63,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Rename environment variable and directory for consistency ([#96]) - `INFS_HOME` → `INFERENCE_HOME` - `~/.infs` → `~/.inference` -- Add `infc` symlink to installed toolchain alongside `inf-llc` and `rust-lld` ([#96]) +- Add `infc` symlink to installed toolchain ([#96]) - Improve `infs install` to auto-set default toolchain when none is configured ([#96]) - When installing an already-installed version without a default toolchain, `infs install` now automatically sets that version as default and updates symlinks - Provides graceful recovery if default toolchain file was manually removed @@ -86,6 +86,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Remove manifest caching from `infs` CLI ([#116]) - `fetch_manifest()` now always fetches from network - Simplifies CLI code; VS Code extension manages its own fetching lifecycle +- Remove LLVM toolchain management from `infs` CLI ([#126]) + - Flatten toolchain layout: `infc` binary now at toolchain root (no more `bin/` subdirectory) + - Remove `inf-llc`, `rust-lld`, and `libLLVM` binary management + - Simplify doctor checks: single `infc` check replaces `inf-llc`, `rust-lld`, and `libLLVM` checks + - Remove platform-specific `#[cfg(target_os = "linux")]` branching in `run_all_checks()` + - Slim `InfsError` to single `ProcessExitCode` variant; all other errors use `anyhow::Result` + - Replace `rand` dependency with lighter-weight `fastrand` + - Remove dead code: unused error variants, `create_project_default()`, `available_versions()`, `selected_bg` theme field ### Build @@ -272,3 +280,4 @@ Initial tagged release. [#97]: https://github.com/Inferara/inference/issues/97 [#116]: https://github.com/Inferara/inference/pull/116 [#125]: https://github.com/Inferara/inference/pull/125 +[#126]: https://github.com/Inferara/inference/pull/126 diff --git a/apps/infs/Cargo.toml b/apps/infs/Cargo.toml index 7a3b88cb..52a9d96a 100644 --- a/apps/infs/Cargo.toml +++ b/apps/infs/Cargo.toml @@ -29,7 +29,7 @@ hex = "0.4" futures-util = "0.3" which = "8.0.0" dirs = "6.0.0" -rand = "0.9.2" +fastrand = "2" ratatui = "0.30.0" crossterm = "0.29.0" anyhow.workspace = true diff --git a/apps/infs/README.md b/apps/infs/README.md index e70124f6..990ce397 100644 --- a/apps/infs/README.md +++ b/apps/infs/README.md @@ -146,15 +146,13 @@ infs doctor **Automatic PATH Configuration:** -On first install, `infs install` automatically adds the toolchain binaries to your system PATH: +On first install, `infs install` automatically adds the toolchain binary to your system PATH: - **Unix (Linux/macOS)**: Modifies shell profile (`~/.bashrc`, `~/.zshrc`, or `~/.config/fish/config.fish`) - **Windows**: Updates user PATH in registry (`HKCU\Environment\Path`) -The toolchain binaries are symlinked to `~/.inference/bin/` and made accessible system-wide: +The toolchain binary is symlinked to `~/.inference/bin/` and made accessible system-wide: - `infc` - Inference compiler -- `inf-llc` - LLVM backend -- `rust-lld` - WebAssembly linker After installation completes, restart your terminal or run: @@ -169,7 +167,7 @@ source ~/.zshrc # Close and reopen terminal ``` -Manual PATH configuration is no longer required. The installed binaries will be available in new terminal sessions. +Manual PATH configuration is no longer required. The installed binary will be available in new terminal sessions. ## Interactive TUI @@ -240,7 +238,7 @@ When running `build`, `run` commands, `infs` locates the `infc` compiler using t |----------|--------|-------------| | 1 (highest) | `INFC_PATH` env var | Explicit path to a specific `infc` binary | | 2 | System PATH | Searches for `infc` in system PATH via `which` | -| 3 (lowest) | Managed toolchain | Uses `~/.inference/toolchains/VERSION/bin/infc` | +| 3 (lowest) | Managed toolchain | Uses `~/.inference/toolchains/VERSION/infc` | ### When to Use Each diff --git a/apps/infs/docs/qa-test-suite.md b/apps/infs/docs/qa-test-suite.md index 8d325715..624bf66c 100644 --- a/apps/infs/docs/qa-test-suite.md +++ b/apps/infs/docs/qa-test-suite.md @@ -2,7 +2,7 @@ This document contains tests that require manual verification or are not yet automated. -> **Automated Tests:** Run `cargo test -p infs` to execute 429 automated tests (360 unit + 69 integration). +> **Automated Tests:** Run `cargo test -p infs` to execute 388 automated tests (320 unit + 68 integration). --- @@ -157,7 +157,6 @@ Test fixtures are located in `apps/infs/tests/fixtures/`: | `example.inf` | Complex example with multiple functions | | `nondet.inf` | Non-deterministic features (forall, exists, assume, unique) | | `syntax_error.inf` | Syntax error handling | -| `type_error.inf` | Type error detection | | `empty.inf` | Empty file edge case | | `uzumaki.inf` | Uzumaki operator (`@`) | | `forall_test.inf` | Forall block with binding | @@ -182,4 +181,4 @@ cargo test -p infs -- --nocapture --- -*Last Updated: 2026-01-27* +*Last Updated: 2026-02-16* diff --git a/apps/infs/src/commands/doctor.rs b/apps/infs/src/commands/doctor.rs index ee05281c..0ddb8268 100644 --- a/apps/infs/src/commands/doctor.rs +++ b/apps/infs/src/commands/doctor.rs @@ -14,9 +14,7 @@ //! - Platform detection //! - Toolchain directory existence //! - Default toolchain configuration -//! - inf-llc binary presence -//! - rust-lld binary presence -//! - libLLVM shared library (Linux only) +//! - infc compiler binary presence //! //! ## Output Format (Public Contract) //! diff --git a/apps/infs/src/commands/self_cmd.rs b/apps/infs/src/commands/self_cmd.rs index abe0646d..3adb6508 100644 --- a/apps/infs/src/commands/self_cmd.rs +++ b/apps/infs/src/commands/self_cmd.rs @@ -124,16 +124,10 @@ async fn execute_update() -> Result<()> { if new_binary_path.exists() { replace_binary(&new_binary_path, platform)?; } else { - let bin_path = temp_dir.join("bin").join(&new_binary_name); - if bin_path.exists() { - replace_binary(&bin_path, platform)?; - } else { - bail!( - "infs binary not found in downloaded archive. Expected at {} or {}", - new_binary_path.display(), - bin_path.display() - ); - } + bail!( + "infs binary not found in downloaded archive. Expected at {}", + new_binary_path.display() + ); } std::fs::remove_file(&download_path).ok(); diff --git a/apps/infs/src/errors.rs b/apps/infs/src/errors.rs index 2a4614e1..7ea5a52a 100644 --- a/apps/infs/src/errors.rs +++ b/apps/infs/src/errors.rs @@ -1,113 +1,10 @@ //! Error types for the infs CLI. -//! -//! This module defines the `InfsError` enum which consolidates all error variants -//! that can occur during CLI operations. While the current implementation primarily -//! uses `anyhow::Result` for error handling, these typed errors enable more precise -//! error handling and better error messages in specific scenarios. -use std::path::PathBuf; use thiserror::Error; -/// Consolidated error type for infs CLI operations. -/// -/// This enum captures all error variants that can occur during compilation -/// and CLI operations. Each variant includes context-specific information -/// to provide helpful error messages to users. +/// Error type for infs CLI operations. #[derive(Debug, Error)] -#[allow(dead_code)] pub enum InfsError { - /// Source file not found at the specified path. - #[error("file not found: {path}")] - FileNotFound { - /// The path that was not found. - path: PathBuf, - }, - - /// Error reading or writing files. - #[error("I/O error: {message}")] - IoError { - /// Description of the I/O operation that failed. - message: String, - /// The underlying I/O error. - #[source] - source: std::io::Error, - }, - - /// Syntax error during parsing phase. - #[error("parse error: {message}")] - ParseError { - /// Description of the parse error. - message: String, - }, - - /// Type checking failed. - #[error("type check error: {message}")] - TypeCheckError { - /// Description of the type error. - message: String, - }, - - /// Semantic analysis failed. - #[error("analysis error: {message}")] - AnalysisError { - /// Description of the analysis error. - message: String, - }, - - /// Code generation failed. - #[error("codegen error: {message}")] - CodegenError { - /// Description of the codegen error. - message: String, - }, - - /// Invalid command line arguments. - #[error("invalid arguments: {message}")] - InvalidArguments { - /// Description of what was invalid. - message: String, - }, - - /// Network error during download. - #[error("download error: {message}")] - DownloadError { - /// Description of the download error. - message: String, - /// The underlying error. - #[source] - source: Option>, - }, - - /// Checksum verification failed. - #[error("checksum mismatch: expected {expected}, got {actual}")] - ChecksumMismatch { - /// The expected checksum. - expected: String, - /// The actual checksum. - actual: String, - }, - - /// Manifest parsing or fetching failed. - #[error("manifest error: {message}")] - ManifestError { - /// Description of the manifest error. - message: String, - }, - - /// Toolchain not found. - #[error("toolchain not found: {version}")] - ToolchainNotFound { - /// The version that was not found. - version: String, - }, - - /// Installation failed. - #[error("installation failed: {message}")] - InstallError { - /// Description of the installation error. - message: String, - }, - /// Subprocess exited with non-zero code. /// /// This variant is used when a subprocess (like wasmtime or coqc) exits @@ -120,117 +17,7 @@ pub enum InfsError { }, } -#[allow(dead_code)] impl InfsError { - /// Creates a new `FileNotFound` error. - #[must_use] - pub fn file_not_found(path: PathBuf) -> Self { - Self::FileNotFound { path } - } - - /// Creates a new `IoError` from an I/O error with context. - #[must_use] - pub fn io_error(message: impl Into, source: std::io::Error) -> Self { - Self::IoError { - message: message.into(), - source, - } - } - - /// Creates a new `ParseError`. - #[must_use] - pub fn parse_error(message: impl Into) -> Self { - Self::ParseError { - message: message.into(), - } - } - - /// Creates a new `TypeCheckError`. - #[must_use] - pub fn type_check_error(message: impl Into) -> Self { - Self::TypeCheckError { - message: message.into(), - } - } - - /// Creates a new `AnalysisError`. - #[must_use] - pub fn analysis_error(message: impl Into) -> Self { - Self::AnalysisError { - message: message.into(), - } - } - - /// Creates a new `CodegenError`. - #[must_use] - pub fn codegen_error(message: impl Into) -> Self { - Self::CodegenError { - message: message.into(), - } - } - - /// Creates a new `InvalidArguments` error. - #[must_use] - pub fn invalid_arguments(message: impl Into) -> Self { - Self::InvalidArguments { - message: message.into(), - } - } - - /// Creates a new `DownloadError`. - #[must_use] - pub fn download_error(message: impl Into) -> Self { - Self::DownloadError { - message: message.into(), - source: None, - } - } - - /// Creates a new `DownloadError` with a source error. - #[must_use] - pub fn download_error_with_source( - message: impl Into, - source: Box, - ) -> Self { - Self::DownloadError { - message: message.into(), - source: Some(source), - } - } - - /// Creates a new `ChecksumMismatch` error. - #[must_use] - pub fn checksum_mismatch(expected: impl Into, actual: impl Into) -> Self { - Self::ChecksumMismatch { - expected: expected.into(), - actual: actual.into(), - } - } - - /// Creates a new `ManifestError`. - #[must_use] - pub fn manifest_error(message: impl Into) -> Self { - Self::ManifestError { - message: message.into(), - } - } - - /// Creates a new `ToolchainNotFound` error. - #[must_use] - pub fn toolchain_not_found(version: impl Into) -> Self { - Self::ToolchainNotFound { - version: version.into(), - } - } - - /// Creates a new `InstallError`. - #[must_use] - pub fn install_error(message: impl Into) -> Self { - Self::InstallError { - message: message.into(), - } - } - /// Creates a new `ProcessExitCode` error. #[must_use] pub const fn process_exit_code(code: i32) -> Self { @@ -242,57 +29,6 @@ impl InfsError { mod tests { use super::*; - #[test] - fn file_not_found_displays_path() { - let err = InfsError::file_not_found(PathBuf::from("/some/path.inf")); - assert_eq!(err.to_string(), "file not found: /some/path.inf"); - } - - #[test] - fn parse_error_displays_message() { - let err = InfsError::parse_error("unexpected token"); - assert_eq!(err.to_string(), "parse error: unexpected token"); - } - - #[test] - fn invalid_arguments_displays_message() { - let err = InfsError::invalid_arguments("missing required flag"); - assert_eq!(err.to_string(), "invalid arguments: missing required flag"); - } - - #[test] - fn download_error_displays_message() { - let err = InfsError::download_error("connection timeout"); - assert_eq!(err.to_string(), "download error: connection timeout"); - } - - #[test] - fn checksum_mismatch_displays_both_values() { - let err = InfsError::checksum_mismatch("abc123", "def456"); - assert_eq!( - err.to_string(), - "checksum mismatch: expected abc123, got def456" - ); - } - - #[test] - fn manifest_error_displays_message() { - let err = InfsError::manifest_error("invalid JSON"); - assert_eq!(err.to_string(), "manifest error: invalid JSON"); - } - - #[test] - fn toolchain_not_found_displays_version() { - let err = InfsError::toolchain_not_found("0.1.0"); - assert_eq!(err.to_string(), "toolchain not found: 0.1.0"); - } - - #[test] - fn install_error_displays_message() { - let err = InfsError::install_error("extraction failed"); - assert_eq!(err.to_string(), "installation failed: extraction failed"); - } - #[test] fn process_exit_code_displays_code() { let err = InfsError::process_exit_code(42); diff --git a/apps/infs/src/main.rs b/apps/infs/src/main.rs index 4368e2e7..5f64d4e3 100644 --- a/apps/infs/src/main.rs +++ b/apps/infs/src/main.rs @@ -84,7 +84,7 @@ COMPILER RESOLUTION: The infc compiler is located using the following priority order: 1. INFC_PATH environment variable (explicit override) 2. System PATH (via 'which infc') - 3. Managed toolchain (~/.inference/toolchains/VERSION/bin/infc) + 3. Managed toolchain (~/.inference/toolchains/VERSION/infc) ENVIRONMENT VARIABLES: INFS_NO_TUI Disable interactive TUI diff --git a/apps/infs/src/project/mod.rs b/apps/infs/src/project/mod.rs index 1f2e77fe..d8618394 100644 --- a/apps/infs/src/project/mod.rs +++ b/apps/infs/src/project/mod.rs @@ -16,10 +16,4 @@ pub mod manifest; pub mod scaffold; -#[allow(unused_imports)] -pub use manifest::validate_project_name; -#[allow(unused_imports)] -pub use manifest::{Dependencies, Package}; -#[allow(unused_imports)] -pub use scaffold::create_project_default; pub use scaffold::{create_project, init_project}; diff --git a/apps/infs/src/project/scaffold.rs b/apps/infs/src/project/scaffold.rs index 38d62edf..a6041c08 100644 --- a/apps/infs/src/project/scaffold.rs +++ b/apps/infs/src/project/scaffold.rs @@ -72,32 +72,6 @@ pub fn create_project(name: &str, parent_path: Option<&Path>, init_git: bool) -> Ok(project_path) } -/// Creates a new Inference project using the default structure. -/// -/// This is a convenience function that calls [`create_project`]. -/// -/// # Arguments -/// -/// * `name` - The project name (used for directory and manifest) -/// * `parent_path` - Optional parent directory (defaults to current directory) -/// * `init_git` - Whether to initialize a git repository -/// -/// # Returns -/// -/// The path to the created project directory. -/// -/// # Errors -/// -/// Returns an error if project creation fails. -#[allow(dead_code)] -pub fn create_project_default( - name: &str, - parent_path: Option<&Path>, - init_git: bool, -) -> Result { - create_project(name, parent_path, init_git) -} - /// Initializes an existing directory as an Inference project. /// /// This function creates manifest and optionally source files in an @@ -341,7 +315,7 @@ mod tests { use std::fs; fn temp_dir() -> PathBuf { - let dir = std::env::temp_dir().join(format!("infs_test_{}", rand::random::())); + let dir = std::env::temp_dir().join(format!("infs_test_{}", fastrand::u64(..))); fs::create_dir_all(&dir).unwrap(); dir } @@ -387,19 +361,6 @@ mod tests { cleanup(&parent); } - #[test] - fn test_create_project_default() { - let parent = temp_dir(); - let result = create_project_default("my_project_default", Some(&parent), false); - - assert!(result.is_ok()); - let project_path = result.unwrap(); - assert!(project_path.exists()); - assert!(project_path.join("Inference.toml").exists()); - - cleanup(&parent); - } - #[test] fn test_create_project_invalid_name() { let parent = temp_dir(); diff --git a/apps/infs/src/toolchain/archive.rs b/apps/infs/src/toolchain/archive.rs index 79032b70..b1bcc5f2 100644 --- a/apps/infs/src/toolchain/archive.rs +++ b/apps/infs/src/toolchain/archive.rs @@ -12,7 +12,7 @@ use tar::Archive; /// /// Creates the destination directory if it does not exist. /// If all archive entries share a common root folder, it is automatically -/// stripped during extraction (e.g., `toolchain-0.2.0/bin/infc` becomes `bin/infc`). +/// stripped during extraction (e.g., `toolchain-0.2.0/infc` becomes `infc`). /// /// # Errors /// @@ -187,7 +187,7 @@ pub fn extract_archive(archive_path: &Path, dest_dir: &Path) -> Result<()> { /// /// Creates the destination directory if it does not exist. /// If all archive entries share a common root folder, it is automatically -/// stripped during extraction (e.g., `toolchain-0.2.0/bin/infc` becomes `bin/infc`). +/// stripped during extraction (e.g., `toolchain-0.2.0/infc` becomes `infc`). /// /// # Errors /// @@ -369,54 +369,22 @@ fn find_common_root_folder( } } -/// Sets executable permissions on binary files within a toolchain directory (Unix only). -/// -/// This function iterates over all files in the `bin` subdirectory of the given -/// directory and sets the executable permission bits (0o755) on each file. +/// Sets executable permissions on the `infc` binary within a toolchain directory (Unix only). /// /// On Windows, this function does nothing since executable permissions are not /// managed the same way. /// /// # Arguments /// -/// * `dir` - The toolchain directory containing a `bin` subdirectory. +/// * `dir` - The toolchain directory containing the `infc` binary at root. /// /// # Errors /// -/// Returns an error if: -/// - The bin directory cannot be read -/// - File metadata cannot be retrieved -/// - Permissions cannot be set -/// -/// # Example -/// -/// ```ignore -/// use crate::toolchain::set_executable_permissions; -/// set_executable_permissions(Path::new("/path/to/toolchain"))?; -/// ``` +/// Returns an error if file metadata cannot be retrieved or permissions cannot be set. #[cfg(unix)] pub fn set_executable_permissions(dir: &Path) -> Result<()> { use std::os::unix::fs::PermissionsExt; - let bin_dir = dir.join("bin"); - if bin_dir.exists() { - let entries = std::fs::read_dir(&bin_dir) - .with_context(|| format!("Failed to read bin directory: {}", bin_dir.display()))?; - - for entry in entries { - let entry = entry.with_context(|| "Failed to read directory entry")?; - let path = entry.path(); - if path.is_file() { - let mut perms = std::fs::metadata(&path) - .with_context(|| format!("Failed to get metadata: {}", path.display()))? - .permissions(); - perms.set_mode(0o755); - std::fs::set_permissions(&path, perms) - .with_context(|| format!("Failed to set permissions: {}", path.display()))?; - } - } - } - let infc_path = dir.join("infc"); if infc_path.is_file() { let mut perms = std::fs::metadata(&infc_path) @@ -448,12 +416,12 @@ mod tests { /// Creates a temporary test directory with a unique name. fn temp_test_dir(name: &str) -> PathBuf { let dir = - std::env::temp_dir().join(format!("infs_test_{}_{}", name, rand::random::())); + std::env::temp_dir().join(format!("infs_test_{}_{}", name, fastrand::u64(..))); std::fs::create_dir_all(&dir).expect("Should create temp dir"); dir } - /// Creates a tar.gz archive with files nested under a root folder. + /// Creates a tar.gz archive with a single file nested under a root folder. fn create_tar_gz_with_root(archive_path: &Path, root_name: &str) { let file = std::fs::File::create(archive_path).expect("Should create file"); let encoder = GzEncoder::new(file, Compression::default()); @@ -466,27 +434,15 @@ mod tests { builder .append_data( &mut header, - format!("{root_name}/bin/infc"), + format!("{root_name}/infc"), b"binary content".as_slice(), ) .expect("Should append file"); - let mut header = tar::Header::new_gnu(); - header.set_size(15); - header.set_mode(0o644); - header.set_cksum(); - builder - .append_data( - &mut header, - format!("{root_name}/lib/libLLVM.so"), - b"library content".as_slice(), - ) - .expect("Should append file"); - builder.finish().expect("Should finish"); } - /// Creates a tar.gz archive with files at the root level (no common folder). + /// Creates a tar.gz archive with a single file at the root level (no common folder). fn create_tar_gz_without_root(archive_path: &Path) { let file = std::fs::File::create(archive_path).expect("Should create file"); let encoder = GzEncoder::new(file, Compression::default()); @@ -497,15 +453,7 @@ mod tests { header.set_mode(0o755); header.set_cksum(); builder - .append_data(&mut header, "bin/infc", b"binary content".as_slice()) - .expect("Should append file"); - - let mut header = tar::Header::new_gnu(); - header.set_size(15); - header.set_mode(0o644); - header.set_cksum(); - builder - .append_data(&mut header, "lib/libLLVM.so", b"library content".as_slice()) + .append_data(&mut header, "infc", b"binary content".as_slice()) .expect("Should append file"); builder.finish().expect("Should finish"); @@ -521,8 +469,7 @@ mod tests { extract_tar_gz(&archive_path, &dest_dir).expect("Should extract"); - assert!(dest_dir.join("bin").join("infc").exists()); - assert!(dest_dir.join("lib").join("libLLVM.so").exists()); + assert!(dest_dir.join("infc").exists()); assert!(!dest_dir.join("root-folder").exists()); let _ = std::fs::remove_dir_all(&temp_dir); @@ -538,8 +485,7 @@ mod tests { extract_tar_gz(&archive_path, &dest_dir).expect("Should extract"); - assert!(dest_dir.join("bin").join("infc").exists()); - assert!(dest_dir.join("lib").join("libLLVM.so").exists()); + assert!(dest_dir.join("infc").exists()); let _ = std::fs::remove_dir_all(&temp_dir); } @@ -554,8 +500,7 @@ mod tests { extract_archive(&archive_path, &dest_dir).expect("Should extract"); - assert!(dest_dir.join("bin").join("infc").exists()); - assert!(dest_dir.join("lib").join("libLLVM.so").exists()); + assert!(dest_dir.join("infc").exists()); let _ = std::fs::remove_dir_all(&temp_dir); } @@ -570,8 +515,7 @@ mod tests { extract_archive(&archive_path, &dest_dir).expect("Should extract"); - assert!(dest_dir.join("bin").join("infc").exists()); - assert!(dest_dir.join("lib").join("libLLVM.so").exists()); + assert!(dest_dir.join("infc").exists()); let _ = std::fs::remove_dir_all(&temp_dir); } @@ -587,21 +531,16 @@ mod tests { let mut zip = zip::ZipWriter::new(file); let options = zip::write::SimpleFileOptions::default(); - zip.start_file("bin/infc", options) + zip.start_file("infc", options) .expect("Should start file"); zip.write_all(b"binary content").expect("Should write"); - zip.start_file("lib/libLLVM.so", options) - .expect("Should start file"); - zip.write_all(b"library content").expect("Should write"); - zip.finish().expect("Should finish"); } extract_archive(&archive_path, &dest_dir).expect("Should extract"); - assert!(dest_dir.join("bin").join("infc").exists()); - assert!(dest_dir.join("lib").join("libLLVM.so").exists()); + assert!(dest_dir.join("infc").exists()); let _ = std::fs::remove_dir_all(&temp_dir); } @@ -618,22 +557,17 @@ mod tests { let mut zip = zip::ZipWriter::new(file); let options = zip::write::SimpleFileOptions::default(); - zip.start_file("root-folder/bin/infc", options) + zip.start_file("root-folder/infc", options) .expect("Should start file"); zip.write_all(b"binary content").expect("Should write"); - zip.start_file("root-folder/lib/libLLVM.so", options) - .expect("Should start file"); - zip.write_all(b"library content").expect("Should write"); - zip.finish().expect("Should finish"); } extract_zip(&archive_path, &dest_dir).expect("Should extract"); // Verify root folder was stripped - assert!(dest_dir.join("bin").join("infc").exists()); - assert!(dest_dir.join("lib").join("libLLVM.so").exists()); + assert!(dest_dir.join("infc").exists()); assert!(!dest_dir.join("root-folder").exists()); // Cleanup @@ -652,22 +586,17 @@ mod tests { let mut zip = zip::ZipWriter::new(file); let options = zip::write::SimpleFileOptions::default(); - zip.start_file("bin/infc", options) + zip.start_file("infc", options) .expect("Should start file"); zip.write_all(b"binary content").expect("Should write"); - zip.start_file("lib/libLLVM.so", options) - .expect("Should start file"); - zip.write_all(b"library content").expect("Should write"); - zip.finish().expect("Should finish"); } extract_zip(&archive_path, &dest_dir).expect("Should extract"); // Verify structure is preserved - assert!(dest_dir.join("bin").join("infc").exists()); - assert!(dest_dir.join("lib").join("libLLVM.so").exists()); + assert!(dest_dir.join("infc").exists()); // Cleanup let _ = std::fs::remove_dir_all(&temp_dir); @@ -897,38 +826,6 @@ mod tests { .append_data(&mut header, "./infc", b"infc binary...".as_slice()) .expect("Should append infc"); - // Add ./bin/ directory - let mut header = tar::Header::new_gnu(); - header.set_entry_type(tar::EntryType::Directory); - header.set_size(0); - header.set_mode(0o755); - header.set_cksum(); - builder - .append_data(&mut header, "./bin/", std::io::empty()) - .expect("Should append bin dir"); - - // Add ./bin/inf-llc - let mut header = tar::Header::new_gnu(); - header.set_size(15); - header.set_mode(0o755); - header.set_cksum(); - builder - .append_data(&mut header, "./bin/inf-llc", b"inf-llc binary.".as_slice()) - .expect("Should append inf-llc"); - - // Add ./bin/rust-lld - let mut header = tar::Header::new_gnu(); - header.set_size(16); - header.set_mode(0o755); - header.set_cksum(); - builder - .append_data( - &mut header, - "./bin/rust-lld", - b"rust-lld binary.".as_slice(), - ) - .expect("Should append rust-lld"); - builder.finish().expect("Should finish"); } @@ -943,19 +840,11 @@ mod tests { extract_tar_gz(&archive_path, &dest_dir).expect("Should extract"); - // Verify all binaries are in expected locations + // Verify infc binary is in expected location assert!( dest_dir.join("infc").exists(), "infc should exist at toolchain root" ); - assert!( - dest_dir.join("bin").join("inf-llc").exists(), - "inf-llc should exist in bin/" - ); - assert!( - dest_dir.join("bin").join("rust-lld").exists(), - "rust-lld should exist in bin/" - ); // Verify ./ directory was NOT created (it should be stripped) assert!( @@ -1066,59 +955,12 @@ mod tests { use super::*; use std::os::unix::fs::PermissionsExt; - #[test] - fn set_executable_permissions_sets_755_on_bin_files() { - let temp_dir = temp_test_dir("exec_perm_bin"); - let bin_dir = temp_dir.join("bin"); - std::fs::create_dir_all(&bin_dir).expect("Should create bin dir"); - - let file1 = bin_dir.join("infc"); - let file2 = bin_dir.join("inf-llc"); - std::fs::write(&file1, b"binary1").expect("Should write file1"); - std::fs::write(&file2, b"binary2").expect("Should write file2"); - - // Set initial non-executable permissions - std::fs::set_permissions(&file1, std::fs::Permissions::from_mode(0o644)) - .expect("Should set initial perms"); - std::fs::set_permissions(&file2, std::fs::Permissions::from_mode(0o644)) - .expect("Should set initial perms"); - - set_executable_permissions(&temp_dir).expect("Should set permissions"); - - let mode1 = std::fs::metadata(&file1) - .expect("Should get metadata") - .permissions() - .mode(); - let mode2 = std::fs::metadata(&file2) - .expect("Should get metadata") - .permissions() - .mode(); - - assert_eq!(mode1 & 0o777, 0o755, "file1 should have 0o755 mode"); - assert_eq!(mode2 & 0o777, 0o755, "file2 should have 0o755 mode"); - - let _ = std::fs::remove_dir_all(&temp_dir); - } - - #[test] - fn set_executable_permissions_handles_missing_bin_dir() { - let temp_dir = temp_test_dir("exec_perm_no_bin"); - // Do not create bin/ subdirectory - - let result = set_executable_permissions(&temp_dir); - - assert!(result.is_ok(), "Should succeed without bin/ directory"); - - let _ = std::fs::remove_dir_all(&temp_dir); - } - #[test] fn set_executable_permissions_sets_755_on_root_infc() { let temp_dir = temp_test_dir("exec_perm_root_infc"); let infc_path = temp_dir.join("infc"); std::fs::write(&infc_path, b"infc binary").expect("Should write infc"); - // Set initial non-executable permissions std::fs::set_permissions(&infc_path, std::fs::Permissions::from_mode(0o644)) .expect("Should set initial perms"); @@ -1133,6 +975,17 @@ mod tests { let _ = std::fs::remove_dir_all(&temp_dir); } + + #[test] + fn set_executable_permissions_handles_missing_infc() { + let temp_dir = temp_test_dir("exec_perm_no_infc"); + + let result = set_executable_permissions(&temp_dir); + + assert!(result.is_ok(), "Should succeed without infc binary"); + + let _ = std::fs::remove_dir_all(&temp_dir); + } } #[test] @@ -1164,10 +1017,6 @@ mod tests { // Verify nested archive was extracted assert!(dest_dir.join("infc").exists(), "infc should exist"); - assert!( - dest_dir.join("bin").join("inf-llc").exists(), - "inf-llc should exist" - ); // tar.gz should be cleaned up assert!( !dest_dir.join("infc-linux-x64.tar.gz").exists(), @@ -1211,10 +1060,6 @@ mod tests { // Verify nested archive was extracted assert!(dest_dir.join("infc").exists(), "infc should exist"); - assert!( - dest_dir.join("bin").join("inf-llc").exists(), - "inf-llc should exist" - ); // Both tar.gz and sha256 should be cleaned up assert!( !dest_dir.join("infc-linux-x64.tar.gz").exists(), diff --git a/apps/infs/src/toolchain/conflict.rs b/apps/infs/src/toolchain/conflict.rs index 832b12f2..73d95cdf 100644 --- a/apps/infs/src/toolchain/conflict.rs +++ b/apps/infs/src/toolchain/conflict.rs @@ -1,7 +1,7 @@ //! PATH conflict detection module. //! -//! This module provides functionality to detect when binaries in the user's PATH -//! shadow the managed toolchain binaries. This helps users understand why the +//! This module provides functionality to detect when a binary in the user's PATH +//! shadows the managed toolchain binary. This helps users understand why the //! managed toolchain might not be used when they run commands. //! //! ## Usage @@ -33,10 +33,10 @@ pub struct PathConflict { pub expected: PathBuf, } -/// Detects PATH conflicts for managed binaries. +/// Detects PATH conflicts for the managed `infc` binary. /// -/// Checks if any of the managed binaries (`infc`, `inf-llc`, `rust-lld`) are found -/// in PATH at a location different from the managed bin directory. +/// Checks if the managed binary is found in PATH at a location different +/// from the managed bin directory. /// /// A conflict is reported when: /// 1. The binary is found in PATH @@ -59,20 +59,18 @@ pub fn detect_path_conflicts(bin_dir: &Path) -> Vec { let mut conflicts = Vec::new(); - for name in ToolchainPaths::MANAGED_BINARIES { - let binary_with_ext = format!("{name}{ext}"); - let expected = bin_dir.join(&binary_with_ext); - - if let Ok(found_path) = which::which(&binary_with_ext) - && found_path != expected - && expected.exists() - { - conflicts.push(PathConflict { - binary: binary_with_ext, - found: found_path, - expected, - }); - } + let binary_with_ext = format!("{}{ext}", ToolchainPaths::MANAGED_BINARY); + let expected = bin_dir.join(&binary_with_ext); + + if let Ok(found_path) = which::which(&binary_with_ext) + && found_path != expected + && expected.exists() + { + conflicts.push(PathConflict { + binary: binary_with_ext, + found: found_path, + expected, + }); } conflicts @@ -269,25 +267,18 @@ mod tests { expected: PathBuf::from("/home/user/.inference/bin/infc"), }, PathConflict { - binary: "inf-llc".to_string(), - found: PathBuf::from("/opt/bin/inf-llc"), - expected: PathBuf::from("/home/user/.inference/bin/inf-llc"), - }, - PathConflict { - binary: "rust-lld".to_string(), - found: PathBuf::from("/another/path/rust-lld"), - expected: PathBuf::from("/home/user/.inference/bin/rust-lld"), + binary: "infs".to_string(), + found: PathBuf::from("/opt/bin/infs"), + expected: PathBuf::from("/home/user/.inference/bin/infs"), }, ]; let warning = format_conflict_warning(&conflicts); assert!(warning.contains("'infc' found at: /usr/local/bin/infc")); - assert!(warning.contains("'inf-llc' found at: /opt/bin/inf-llc")); - assert!(warning.contains("'rust-lld' found at: /another/path/rust-lld")); + assert!(warning.contains("'infs' found at: /opt/bin/infs")); assert!(warning.contains("Expected: /home/user/.inference/bin/infc")); - assert!(warning.contains("Expected: /home/user/.inference/bin/inf-llc")); - assert!(warning.contains("Expected: /home/user/.inference/bin/rust-lld")); + assert!(warning.contains("Expected: /home/user/.inference/bin/infs")); } #[test] @@ -306,9 +297,9 @@ mod tests { expected: PathBuf::from("/home/user/.inference/bin/infc"), }, PathConflict { - binary: "inf-llc".to_string(), - found: PathBuf::from("/opt/bin/inf-llc"), - expected: PathBuf::from("/home/user/.inference/bin/inf-llc"), + binary: "infs".to_string(), + found: PathBuf::from("/opt/bin/infs"), + expected: PathBuf::from("/home/user/.inference/bin/infs"), }, ]; @@ -322,7 +313,7 @@ mod tests { assert!( lines .iter() - .any(|l| l.contains("'inf-llc' resolves to /opt/bin/inf-llc")) + .any(|l| l.contains("'infs' resolves to /opt/bin/infs")) ); assert!( lines @@ -332,7 +323,7 @@ mod tests { assert!( lines .iter() - .any(|l| l.contains("managed version is at /home/user/.inference/bin/inf-llc")) + .any(|l| l.contains("managed version is at /home/user/.inference/bin/infs")) ); assert!(lines.iter().any(|l| l.contains("Fix:"))); } diff --git a/apps/infs/src/toolchain/doctor.rs b/apps/infs/src/toolchain/doctor.rs index b0ef0853..7e8736c1 100644 --- a/apps/infs/src/toolchain/doctor.rs +++ b/apps/infs/src/toolchain/doctor.rs @@ -9,9 +9,7 @@ //! - Platform detection //! - Toolchain directory existence //! - Default toolchain configuration -//! - `inf-llc` binary presence -//! - `rust-lld` binary presence -//! - `libLLVM` shared library (Linux only) +//! - `infc` compiler binary presence use super::{Platform, ToolchainPaths}; @@ -100,29 +98,13 @@ impl DoctorCheck { /// Runs all doctor checks and returns the results. /// /// This function aggregates all health checks into a single vector. -/// On Linux, it additionally includes the `libLLVM` check. -#[cfg(not(target_os = "linux"))] pub fn run_all_checks() -> Vec { vec![ check_infs_binary(), check_platform(), check_toolchain_directory(), check_default_toolchain(), - check_inf_llc(), - check_rust_lld(), - ] -} - -#[cfg(target_os = "linux")] -pub fn run_all_checks() -> Vec { - vec![ - check_infs_binary(), - check_platform(), - check_toolchain_directory(), - check_default_toolchain(), - check_inf_llc(), - check_rust_lld(), - check_libllvm(), + check_infc(), ] } @@ -208,51 +190,39 @@ pub fn check_default_toolchain() -> DoctorCheck { } } -/// Checks if the inf-llc binary is available. +/// Checks if the infc compiler binary is available. #[must_use] -pub fn check_inf_llc() -> DoctorCheck { - check_binary("inf-llc", "inf-llc") -} - -/// Checks if the rust-lld binary is available. -#[must_use] -pub fn check_rust_lld() -> DoctorCheck { - check_binary("rust-lld", "rust-lld") -} - -/// Checks if a binary is available in PATH or the toolchain bin directory. -#[must_use] -pub fn check_binary(name: &str, binary_name: &str) -> DoctorCheck { +pub fn check_infc() -> DoctorCheck { let Ok(platform) = Platform::detect() else { - return DoctorCheck::error(name, "Cannot detect platform"); + return DoctorCheck::error("infc", "Cannot detect platform"); }; - let binary_with_ext = format!("{binary_name}{}", platform.executable_extension()); + let binary_with_ext = format!("infc{}", platform.executable_extension()); if which::which(&binary_with_ext).is_ok() { - return DoctorCheck::ok(name, format!("Found {binary_with_ext} in PATH")); + return DoctorCheck::ok("infc", format!("Found {binary_with_ext} in PATH")); } let Ok(paths) = ToolchainPaths::new() else { - return DoctorCheck::error(name, "Cannot determine toolchain paths"); + return DoctorCheck::error("infc", "Cannot determine toolchain paths"); }; let default_version = match paths.get_default_version() { Ok(Some(v)) => v, Ok(None) => { - return DoctorCheck::warning(name, no_default_toolchain_message(&paths)); + return DoctorCheck::warning("infc", no_default_toolchain_message(&paths)); } Err(_) => { - return DoctorCheck::error(name, "Cannot read default version"); + return DoctorCheck::error("infc", "Cannot read default version"); } }; let binary_path = paths.binary_path(&default_version, &binary_with_ext); if binary_path.exists() { - DoctorCheck::ok(name, format!("Found at {}", binary_path.display())) + DoctorCheck::ok("infc", format!("Found at {}", binary_path.display())) } else { DoctorCheck::error( - name, + "infc", format!( "Not found. Expected at {}. Run 'infs install' to install the toolchain.", binary_path.display() @@ -261,57 +231,6 @@ pub fn check_binary(name: &str, binary_name: &str) -> DoctorCheck { } } -/// Checks if libLLVM is available (Linux only). -#[cfg(target_os = "linux")] -#[must_use] -pub fn check_libllvm() -> DoctorCheck { - let Ok(paths) = ToolchainPaths::new() else { - return DoctorCheck::error("libLLVM", "Cannot determine toolchain paths"); - }; - - let default_version = match paths.get_default_version() { - Ok(Some(v)) => v, - Ok(None) => { - return DoctorCheck::warning("libLLVM", no_default_toolchain_message(&paths)); - } - Err(_) => { - return DoctorCheck::error("libLLVM", "Cannot read default version"); - } - }; - - let lib_dir = paths.toolchain_dir(&default_version).join("lib"); - - if !lib_dir.exists() { - return DoctorCheck::warning( - "libLLVM", - format!("Library directory not found at {}", lib_dir.display()), - ); - } - - let Ok(entries) = std::fs::read_dir(&lib_dir) else { - return DoctorCheck::warning( - "libLLVM", - format!("Cannot read library directory: {}", lib_dir.display()), - ); - }; - - for entry in entries.flatten() { - let name = entry.file_name(); - let name_str = name.to_string_lossy(); - if name_str.starts_with("libLLVM") && name_str.contains(".so") { - return DoctorCheck::ok("libLLVM", format!("Found {}", entry.path().display())); - } - } - - DoctorCheck::warning( - "libLLVM", - format!( - "Not found in {}. Some features may not work.", - lib_dir.display() - ), - ) -} - #[cfg(test)] mod tests { use super::*; @@ -343,12 +262,8 @@ mod tests { #[test] fn run_all_checks_returns_expected_count() { let checks = run_all_checks(); - // Base checks: infs, platform, toolchain dir, default toolchain, inf-llc, rust-lld - #[cfg(not(target_os = "linux"))] - assert_eq!(checks.len(), 6); - // On Linux, libLLVM is also checked - #[cfg(target_os = "linux")] - assert_eq!(checks.len(), 7); + // Checks: infs, platform, toolchain dir, default toolchain, infc + assert_eq!(checks.len(), 5); } #[test] @@ -357,4 +272,66 @@ mod tests { assert!(!check.name.is_empty()); assert!(!check.message.is_empty()); } + + #[test] + fn check_infc_returns_valid_doctor_check() { + let check = check_infc(); + assert_eq!(check.name, "infc"); + assert!(!check.message.is_empty()); + // On dev machines, infc may or may not be available. + // We verify the check returns a valid status regardless of installation state. + assert!( + check.status == DoctorCheckStatus::Ok + || check.status == DoctorCheckStatus::Warning + || check.status == DoctorCheckStatus::Error + ); + } + + #[test] + fn check_infs_binary_returns_valid_doctor_check() { + let check = check_infs_binary(); + assert!(!check.name.is_empty()); + assert!(!check.message.is_empty()); + } + + #[test] + fn check_toolchain_directory_returns_valid_doctor_check() { + let check = check_toolchain_directory(); + assert!(!check.name.is_empty()); + assert!(!check.message.is_empty()); + } + + #[test] + fn check_default_toolchain_returns_valid_doctor_check() { + let check = check_default_toolchain(); + assert!(!check.name.is_empty()); + assert!(!check.message.is_empty()); + } + + #[test] + fn no_default_toolchain_message_with_no_versions() { + let temp_dir = std::env::temp_dir().join("infs_test_doctor_no_default"); + let _ = std::fs::remove_dir_all(&temp_dir); + let paths = ToolchainPaths::with_root(temp_dir.clone()); + std::fs::create_dir_all(&paths.toolchains).unwrap(); + + let msg = no_default_toolchain_message(&paths); + assert!(msg.contains("infs install")); + + std::fs::remove_dir_all(&temp_dir).ok(); + } + + #[test] + fn no_default_toolchain_message_with_installed_versions() { + let temp_dir = std::env::temp_dir().join("infs_test_doctor_no_default_installed"); + let _ = std::fs::remove_dir_all(&temp_dir); + let paths = ToolchainPaths::with_root(temp_dir.clone()); + std::fs::create_dir_all(paths.toolchain_dir("0.1.0")).unwrap(); + + let msg = no_default_toolchain_message(&paths); + assert!(msg.contains("infs default")); + assert!(msg.contains("0.1.0")); + + std::fs::remove_dir_all(&temp_dir).ok(); + } } diff --git a/apps/infs/src/toolchain/download.rs b/apps/infs/src/toolchain/download.rs index c67d5408..b6d6b8e0 100644 --- a/apps/infs/src/toolchain/download.rs +++ b/apps/infs/src/toolchain/download.rs @@ -22,7 +22,6 @@ use std::time::Instant; use anyhow::{Context, Result, bail}; use futures_util::StreamExt; -use rand::Rng; use tokio::io::AsyncWriteExt; /// Progress event emitted during downloads. @@ -249,7 +248,7 @@ fn format_speed(speed: f64) -> String { fn calculate_retry_delay(attempt: u32) -> u64 { let base_delay = BASE_RETRY_DELAY_MS * 2u64.pow(attempt); let jitter_range = base_delay / 4; - let jitter = rand::rng().random_range(0..=jitter_range * 2); + let jitter = fastrand::u64(0..=jitter_range * 2); base_delay - jitter_range + jitter } diff --git a/apps/infs/src/toolchain/manifest.rs b/apps/infs/src/toolchain/manifest.rs index 88231167..52722c8f 100644 --- a/apps/infs/src/toolchain/manifest.rs +++ b/apps/infs/src/toolchain/manifest.rs @@ -264,21 +264,6 @@ pub fn find_version<'a>(manifest: &'a Manifest, version: &str) -> Option<&'a Ver manifest.iter().find(|v| v.version == version) } -/// Returns all available version strings from the manifest. -/// -/// # Arguments -/// -/// * `manifest` - The manifest to query -/// -/// # Returns -/// -/// A vector of version strings. -#[must_use = "returns version list without side effects"] -#[allow(dead_code)] -pub fn available_versions(manifest: &Manifest) -> Vec<&str> { - manifest.iter().map(|v| v.version.as_str()).collect() -} - /// Returns versions sorted by semver (newest first). /// /// Versions that cannot be parsed as semver are sorted lexicographically @@ -549,18 +534,6 @@ mod tests { assert!(version.find_infc_artifact(Platform::WindowsX64).is_none()); } - #[test] - fn available_versions_returns_all() { - let manifest: Manifest = - serde_json::from_str(sample_manifest_json()).expect("Should parse manifest"); - - let versions = available_versions(&manifest); - assert_eq!(versions.len(), 3); - assert!(versions.contains(&"0.1.0")); - assert!(versions.contains(&"0.2.0")); - assert!(versions.contains(&"0.3.0-alpha")); - } - #[test] fn version_entry_has_platform_returns_true_for_existing() { let manifest: Manifest = diff --git a/apps/infs/src/toolchain/paths.rs b/apps/infs/src/toolchain/paths.rs index 399db7b8..0dc01e1e 100644 --- a/apps/infs/src/toolchain/paths.rs +++ b/apps/infs/src/toolchain/paths.rs @@ -10,22 +10,17 @@ //! ~/.inference/ # Root directory (or INFERENCE_HOME) //! toolchains/ # Installed toolchain versions //! 0.1.0/ # Version-specific installation -//! infc # Compiler binary (at root level) -//! bin/ -//! inf-llc # LLVM backend tools -//! rust-lld +//! infc # Compiler binary //! .metadata.json # Installation metadata (date, etc.) //! 0.2.0/ //! ... -//! bin/ # Symlinks to default toolchain binaries +//! bin/ # Symlink to default toolchain binary //! downloads/ # Download cache //! cache/ # Cached data (manifest, etc.) //! default # File containing default version string //! ``` //! -//! Note: Binaries are searched first in the `bin/` subdirectory, then at the -//! toolchain root. This supports both legacy layouts (all in `bin/`) and the -//! current layout (`infc` at root, tools in `bin/`). +//! Binaries are located at the toolchain root directory (e.g., `infc` at root). use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; @@ -263,15 +258,15 @@ pub struct ToolchainPaths { pub root: PathBuf, /// Directory containing installed toolchain versions. pub toolchains: PathBuf, - /// Directory for binary symlinks to the default toolchain. + /// Directory for binary symlink to the default toolchain. pub bin: PathBuf, /// Directory for cached downloads. pub downloads: PathBuf, } impl ToolchainPaths { - /// Names of binaries managed by the toolchain. - pub const MANAGED_BINARIES: [&'static str; 3] = ["infc", "inf-llc", "rust-lld"]; + /// Name of the binary managed by the toolchain. + pub const MANAGED_BINARY: &str = "infc"; /// Creates a new `ToolchainPaths` instance. /// @@ -325,12 +320,6 @@ impl ToolchainPaths { self.toolchains.join(version) } - /// Returns the path to the bin directory within a specific toolchain version. - #[must_use = "returns the path without side effects"] - pub fn toolchain_bin_dir(&self, version: &str) -> PathBuf { - self.toolchain_dir(version).join("bin") - } - /// Returns the path to the file storing the default toolchain version. #[must_use = "returns the path without side effects"] pub fn default_file(&self) -> PathBuf { @@ -479,23 +468,11 @@ impl ToolchainPaths { /// Returns the path to a specific binary within a toolchain version. /// - /// The binary is searched in two locations: - /// 1. First, check the `bin/` subdirectory (e.g., `~/.inference/toolchains/0.0.1/bin/inf-llc`) - /// 2. If not found, check the toolchain root directory (e.g., `~/.inference/toolchains/0.0.1/infc`) - /// 3. If neither exists, return the `bin/` path for consistent error messages + /// The binary is located at the toolchain root directory + /// (e.g., `~/.inference/toolchains/0.0.1/infc`). #[must_use = "returns the path without side effects"] pub fn binary_path(&self, version: &str, binary_name: &str) -> PathBuf { - let bin_path = self.toolchain_bin_dir(version).join(binary_name); - if bin_path.exists() { - return bin_path; - } - - let root_path = self.toolchain_dir(version).join(binary_name); - if root_path.exists() { - return root_path; - } - - bin_path + self.toolchain_dir(version).join(binary_name) } /// Returns the path to a symlinked binary in the global bin directory. @@ -579,11 +556,11 @@ impl ToolchainPaths { /// Updates symlinks in the bin directory to point to the specified version. /// - /// Creates symlinks for `infc`, `inf-llc`, and `rust-lld` binaries. + /// Creates a symlink for the `infc` binary. /// /// # Errors /// - /// Returns an error if the symlinks cannot be created. + /// Returns an error if the symlink cannot be created. pub fn update_symlinks(&self, version: &str) -> Result<()> { let platform = crate::toolchain::Platform::detect()?; let ext = platform.executable_extension(); @@ -591,29 +568,25 @@ impl ToolchainPaths { std::fs::create_dir_all(&self.bin) .with_context(|| format!("Failed to create bin directory: {}", self.bin.display()))?; - for name in Self::MANAGED_BINARIES { - let binary = format!("{name}{ext}"); - self.create_symlink(version, &binary)?; - } + let binary = format!("{}{ext}", Self::MANAGED_BINARY); + self.create_symlink(version, &binary)?; Ok(()) } - /// Removes all symlinks from the bin directory. + /// Removes the symlink from the bin directory. /// - /// Removes symlinks for `infc`, `inf-llc`, and `rust-lld` binaries. + /// Removes the symlink for the `infc` binary. /// /// # Errors /// - /// Returns an error if the symlinks cannot be removed. + /// Returns an error if the symlink cannot be removed. pub fn remove_symlinks(&self) -> Result<()> { let platform = crate::toolchain::Platform::detect()?; let ext = platform.executable_extension(); - for name in Self::MANAGED_BINARIES { - let binary = format!("{name}{ext}"); - self.remove_symlink(&binary)?; - } + let binary = format!("{}{ext}", Self::MANAGED_BINARY); + self.remove_symlink(&binary)?; Ok(()) } @@ -629,14 +602,11 @@ impl ToolchainPaths { let ext = platform.executable_extension(); let mut broken = Vec::new(); - for name in Self::MANAGED_BINARIES { - let binary = format!("{name}{ext}"); - let symlink_path = self.symlink_path(&binary); + let binary = format!("{}{ext}", Self::MANAGED_BINARY); + let symlink_path = self.symlink_path(&binary); - // Check if symlink exists (as a symlink, even if broken) but target does not - if symlink_path.symlink_metadata().is_ok() && !symlink_path.exists() { - broken.push(binary); - } + if symlink_path.symlink_metadata().is_ok() && !symlink_path.exists() { + broken.push(binary); } broken } @@ -894,4 +864,121 @@ mod tests { std::fs::remove_dir_all(&temp_dir).ok(); } + + #[test] + fn managed_binary_is_infc() { + assert_eq!(ToolchainPaths::MANAGED_BINARY, "infc"); + } + + #[test] + fn binary_path_returns_toolchain_root_path() { + let temp_dir = env::temp_dir().join("infs_test_binary_path"); + let paths = ToolchainPaths::with_root(temp_dir.clone()); + let path = paths.binary_path("0.1.0", "infc"); + assert_eq!( + path, + temp_dir.join("toolchains").join("0.1.0").join("infc") + ); + } + + #[test] + fn update_symlinks_creates_symlink_for_managed_binary() { + let temp_dir = env::temp_dir().join("infs_test_update_symlinks"); + let _ = std::fs::remove_dir_all(&temp_dir); + let paths = ToolchainPaths::with_root(temp_dir.clone()); + + let toolchain_dir = paths.toolchain_dir("0.1.0"); + std::fs::create_dir_all(&toolchain_dir).unwrap(); + std::fs::create_dir_all(&paths.bin).unwrap(); + + let platform = crate::toolchain::Platform::detect().unwrap(); + let ext = platform.executable_extension(); + let binary_name = format!("{}{ext}", ToolchainPaths::MANAGED_BINARY); + let source = toolchain_dir.join(&binary_name); + std::fs::write(&source, b"fake binary").unwrap(); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&source, std::fs::Permissions::from_mode(0o755)).unwrap(); + } + + paths.update_symlinks("0.1.0").unwrap(); + + let symlink = paths.symlink_path(&binary_name); + assert!(symlink.exists(), "Symlink should exist after update_symlinks"); + + std::fs::remove_dir_all(&temp_dir).ok(); + } + + #[test] + fn remove_symlinks_removes_managed_binary_symlink() { + let temp_dir = env::temp_dir().join("infs_test_remove_symlinks"); + let _ = std::fs::remove_dir_all(&temp_dir); + let paths = ToolchainPaths::with_root(temp_dir.clone()); + + let toolchain_dir = paths.toolchain_dir("0.1.0"); + std::fs::create_dir_all(&toolchain_dir).unwrap(); + std::fs::create_dir_all(&paths.bin).unwrap(); + + let platform = crate::toolchain::Platform::detect().unwrap(); + let ext = platform.executable_extension(); + let binary_name = format!("{}{ext}", ToolchainPaths::MANAGED_BINARY); + let source = toolchain_dir.join(&binary_name); + std::fs::write(&source, b"fake binary").unwrap(); + + #[cfg(unix)] + { + use std::os::unix::fs::PermissionsExt; + std::fs::set_permissions(&source, std::fs::Permissions::from_mode(0o755)).unwrap(); + } + + paths.update_symlinks("0.1.0").unwrap(); + let symlink = paths.symlink_path(&binary_name); + assert!(symlink.exists(), "Symlink should exist before removal"); + + paths.remove_symlinks().unwrap(); + assert!( + !symlink.exists(), + "Symlink should not exist after remove_symlinks" + ); + + std::fs::remove_dir_all(&temp_dir).ok(); + } + + #[test] + fn validate_symlinks_returns_empty_when_no_broken_links() { + let temp_dir = env::temp_dir().join("infs_test_validate_no_broken"); + let _ = std::fs::remove_dir_all(&temp_dir); + let paths = ToolchainPaths::with_root(temp_dir.clone()); + + std::fs::create_dir_all(&paths.bin).unwrap(); + + let broken = paths.validate_symlinks(); + assert!(broken.is_empty()); + + std::fs::remove_dir_all(&temp_dir).ok(); + } + + #[cfg(unix)] + #[test] + fn validate_symlinks_detects_broken_symlink() { + let temp_dir = env::temp_dir().join("infs_test_validate_broken"); + let _ = std::fs::remove_dir_all(&temp_dir); + let paths = ToolchainPaths::with_root(temp_dir.clone()); + + std::fs::create_dir_all(&paths.bin).unwrap(); + + let binary_name = ToolchainPaths::MANAGED_BINARY; + let symlink_target = paths.symlink_path(binary_name); + let nonexistent = temp_dir.join("nonexistent_binary"); + std::os::unix::fs::symlink(&nonexistent, &symlink_target).unwrap(); + + let broken = paths.validate_symlinks(); + assert_eq!(broken.len(), 1); + assert_eq!(broken[0], binary_name); + + std::fs::remove_dir_all(&temp_dir).ok(); + } + } diff --git a/apps/infs/src/toolchain/resolver.rs b/apps/infs/src/toolchain/resolver.rs index e779f28f..8c3db724 100644 --- a/apps/infs/src/toolchain/resolver.rs +++ b/apps/infs/src/toolchain/resolver.rs @@ -5,7 +5,7 @@ //! //! 1. Explicit override via `INFC_PATH` environment variable //! 2. System PATH via `which::which("infc")` -//! 3. Managed toolchain at `~/.inference/toolchains/VERSION/bin/infc` +//! 3. Managed toolchain at `~/.inference/toolchains/VERSION/infc` //! //! ## Environment Variables //! @@ -36,7 +36,7 @@ const INFC_PATH_ENV: &str = "INFC_PATH"; /// 1. **`INFC_PATH` environment variable** - Explicit override for testing /// or custom installations /// 2. **System PATH** - Uses `which::which("infc")` to find infc in PATH -/// 3. **Managed toolchain** - Looks in `~/.inference/toolchains/VERSION/bin/infc` +/// 3. **Managed toolchain** - Looks in `~/.inference/toolchains/VERSION/infc` /// using the default toolchain version if set /// /// # Errors @@ -81,7 +81,7 @@ pub fn find_infc() -> Result { Platform::detect().context("Failed to detect platform while searching for infc")?; let ext = platform.executable_extension(); let infc_name = format!("infc{ext}"); - let infc_path = paths.toolchain_bin_dir(&version).join(&infc_name); + let infc_path = paths.binary_path(&version, &infc_name); if infc_path.exists() { return Ok(infc_path); diff --git a/apps/infs/src/tui/theme.rs b/apps/infs/src/tui/theme.rs index 1c86722d..db9fe1be 100644 --- a/apps/infs/src/tui/theme.rs +++ b/apps/infs/src/tui/theme.rs @@ -26,9 +26,6 @@ pub struct Theme { pub muted: Color, /// Color for primary text. pub text: Color, - /// Background color for selected items. - #[allow(dead_code)] - pub selected_bg: Color, } impl Default for Theme { @@ -52,7 +49,6 @@ impl Theme { error: Color::Red, muted: Color::DarkGray, text: Color::White, - selected_bg: Color::DarkGray, } } @@ -70,7 +66,6 @@ impl Theme { error: Color::Rgb(139, 0, 0), // Dark red muted: Color::Gray, text: Color::Black, - selected_bg: Color::LightYellow, } } diff --git a/apps/infs/src/tui/views/doctor_view.rs b/apps/infs/src/tui/views/doctor_view.rs index bf52a5bc..ee38eb1e 100644 --- a/apps/infs/src/tui/views/doctor_view.rs +++ b/apps/infs/src/tui/views/doctor_view.rs @@ -179,7 +179,7 @@ mod tests { checks: vec![ DoctorCheck::ok("Platform", "linux x64"), DoctorCheck::warning("Toolchain", "No default set"), - DoctorCheck::error("inf-llc", "Not found"), + DoctorCheck::error("infc", "Not found"), ], selected: 0, loaded: true, @@ -219,9 +219,9 @@ mod tests { let theme = Theme::dark(); let state = DoctorState { checks: vec![ - DoctorCheck::error("inf-llc", "Not found"), - DoctorCheck::error("rust-lld", "Not found"), DoctorCheck::error("infc", "Not found"), + DoctorCheck::error("Toolchain directory", "Not found"), + DoctorCheck::error("Default toolchain", "Not set"), ], selected: 2, loaded: true, diff --git a/apps/infs/src/tui/views/toolchain_view.rs b/apps/infs/src/tui/views/toolchain_view.rs index 87820d7e..0d5464e2 100644 --- a/apps/infs/src/tui/views/toolchain_view.rs +++ b/apps/infs/src/tui/views/toolchain_view.rs @@ -52,7 +52,7 @@ fn render_toolchain_list(frame: &mut Frame, area: Rect, theme: &Theme, state: &T .add_modifier(Modifier::BOLD), ), Span::styled( - "Install latest toolchain", + "Install toolchain", Style::default().fg(theme.text).add_modifier(Modifier::BOLD), ), ])); diff --git a/apps/infs/src/tui/widgets/mod.rs b/apps/infs/src/tui/widgets/mod.rs index d5588998..e444d7da 100644 --- a/apps/infs/src/tui/widgets/mod.rs +++ b/apps/infs/src/tui/widgets/mod.rs @@ -2,7 +2,6 @@ //! //! This module provides reusable widget components for the TUI: //! -//! - [`logo`] - Styled logo rendering with theme support -//! - [`input_field`] - Advanced input field with cursor support +//! - [`command_history`] - Command history display and navigation pub mod command_history; diff --git a/apps/infs/tests/cli_integration.rs b/apps/infs/tests/cli_integration.rs index fca26533..cd3521bd 100644 --- a/apps/infs/tests/cli_integration.rs +++ b/apps/infs/tests/cli_integration.rs @@ -305,20 +305,6 @@ fn build_full_pipeline_with_v_output() { // Version and Help Tests // ============================================================================= -/// Verifies that the `version` subcommand displays the correct version information. -/// -/// **Expected behavior**: Exit with code 0 and print the version string to stdout. -#[test] -fn version_command_shows_version() { - let mut cmd = Command::new(assert_cmd::cargo::cargo_bin!("infs")); - cmd.arg("version"); - - cmd.assert() - .success() - .stdout(predicate::str::contains("infs")) - .stdout(predicate::str::contains(env!("CARGO_PKG_VERSION"))); -} - /// Verifies that the `--version` flag displays the correct version information. /// /// **Expected behavior**: Exit with code 0 and print the version string to stdout. @@ -787,8 +773,7 @@ fn doctor_shows_all_checks() { .stdout(predicate::str::contains("Platform")) .stdout(predicate::str::contains("Toolchain directory")) .stdout(predicate::str::contains("Default toolchain")) - .stdout(predicate::str::contains("inf-llc")) - .stdout(predicate::str::contains("rust-lld")); + .stdout(predicate::str::contains("infc")); } /// Verifies that `infs doctor` shows the checking message. @@ -1532,12 +1517,6 @@ fn build_fails_gracefully_on_syntax_error() { // Helper Functions for QA Test Files // ============================================================================= -/// Returns the path to `type_error.inf` test file. -#[allow(dead_code)] -fn type_error_file() -> std::path::PathBuf { - example_file("type_error.inf") -} - /// Returns the path to `empty.inf` test file. fn empty_file() -> std::path::PathBuf { example_file("empty.inf") diff --git a/apps/infs/tests/fixtures/type_error.inf b/apps/infs/tests/fixtures/type_error.inf deleted file mode 100644 index 7e85de47..00000000 --- a/apps/infs/tests/fixtures/type_error.inf +++ /dev/null @@ -1,3 +0,0 @@ -fn mistyped() -> i32 { - "not an integer" -} diff --git a/core/inference/src/lib.rs b/core/inference/src/lib.rs index 97771a49..9b362b85 100644 --- a/core/inference/src/lib.rs +++ b/core/inference/src/lib.rs @@ -259,7 +259,6 @@ //! - [Inference Language Specification](https://github.com/Inferara/inference-language-spec) //! - [Inference Book](https://github.com/Inferara/book) //! - [Tree-sitter Grammar](https://github.com/Inferara/tree-sitter-inference) -//! - [Non-deterministic Instruction Extensions](https://github.com/Inferara/llvm-project/pull/2) use inference_ast::{arena::Arena, builder::Builder}; use inference_type_checker::typed_context::TypedContext; diff --git a/editors/vscode/QA_GUIDE.md b/editors/vscode/QA_GUIDE.md index adc6c76a..f5e88f26 100644 --- a/editors/vscode/QA_GUIDE.md +++ b/editors/vscode/QA_GUIDE.md @@ -88,7 +88,7 @@ Many QA cases below are covered by automated tests (`npm test`). Cases marked wi | 3.1 | Observe status bar immediately after activation | Status bar shows `$(loading~spin) Inference`. Tooltip: "Checking toolchain..." **[A]** initial state tested | | | 3.2 | Activate extension with **no** toolchain installed | Status bar shows `$(dash) Inference` (grey). Tooltip: "Inference: Toolchain not found. Click to run doctor." **[A]** | | | 3.3 | Activate extension with a **healthy** toolchain | Status bar shows `$(check) Inference`. Tooltip: "Inference: Toolchain healthy" **[A]** | | -| 3.4 | Activate with toolchain that has **warnings** (e.g., missing libLLVM) | Status bar shows `$(warning) Inference` (warning background). Tooltip shows doctor summary. **[A]** | | +| 3.4 | Activate with toolchain that has **warnings** (e.g., infc found but not in managed location) | Status bar shows `$(warning) Inference` (warning background). Tooltip shows doctor summary. **[A]** | | | 3.5 | Activate with toolchain that has **errors** | Status bar shows `$(error) Inference` (error background). Tooltip shows doctor summary. **[A]** | | | 3.6 | Click the status bar item | Runs `inference.runDoctor` command | |