diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..bd5fcfa --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,220 @@ +name: Build and Test + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + workflow_dispatch: + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + strategy: + fail-fast: false + matrix: + platform: + - os: ubuntu-22.04 + rust_target: x86_64-unknown-linux-gnu + - os: macos-latest + rust_target: aarch64-apple-darwin + # Windows disabled temporarily - needs import library generation work + # - os: windows-latest + # rust_target: x86_64-pc-windows-msvc + + runs-on: ${{ matrix.platform.os }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install dependencies (Ubuntu) + if: matrix.platform.os == 'ubuntu-22.04' + run: | + sudo apt-get update + sudo apt-get install -y \ + libwebkit2gtk-4.1-dev \ + build-essential \ + curl \ + wget \ + file \ + libssl-dev \ + libayatana-appindicator3-dev \ + librsvg2-dev + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.platform.rust_target }} + + - name: Rust cache + uses: Swatinem/rust-cache@v2 + with: + workspaces: src-tauri -> target + + - name: Download llama.cpp libraries (Linux) + if: matrix.platform.os == 'ubuntu-22.04' + run: | + wget https://github.com/ggml-org/llama.cpp/releases/download/b7423/llama-b7423-bin-ubuntu-x64.tar.gz + tar -xzf llama-b7423-bin-ubuntu-x64.tar.gz + mkdir -p src-tauri/libs/linux-x64 + cp -P llama-b7423/*.so* src-tauri/libs/linux-x64/ 2>/dev/null || true + ls -la src-tauri/libs/linux-x64/ + + - name: Download llama.cpp libraries (macOS) + if: matrix.platform.os == 'macos-latest' + run: | + wget https://github.com/ggml-org/llama.cpp/releases/download/b7423/llama-b7423-bin-macos-arm64.tar.gz + tar -xzf llama-b7423-bin-macos-arm64.tar.gz + mkdir -p src-tauri/libs/darwin-arm64 + cp -P llama-b7423/*.dylib src-tauri/libs/darwin-arm64/ + ls -la src-tauri/libs/darwin-arm64/ + + - name: Download llama.cpp libraries (Windows) + if: matrix.platform.os == 'windows-latest' + shell: powershell + run: | + Invoke-WebRequest -Uri "https://github.com/ggml-org/llama.cpp/releases/download/b7423/llama-b7423-bin-win-vulkan-x64.zip" -OutFile "llama-libs.zip" + Expand-Archive -Path "llama-libs.zip" -DestinationPath "llama-extracted" + Get-ChildItem -Recurse llama-extracted | Select-Object FullName + New-Item -ItemType Directory -Force -Path src-tauri/libs/windows-x64-vulkan + Get-ChildItem -Path "llama-extracted" -Recurse -Filter "*.dll" | Copy-Item -Destination src-tauri/libs/windows-x64-vulkan/ + Get-ChildItem -Path "llama-extracted" -Recurse -Filter "*.lib" | Copy-Item -Destination src-tauri/libs/windows-x64-vulkan/ + Get-ChildItem src-tauri/libs/windows-x64-vulkan/ + + - name: Generate import libraries from DLLs (Windows) + if: matrix.platform.os == 'windows-latest' + shell: powershell + run: | + $libDir = "src-tauri\libs\windows-x64-vulkan" + + # Find Visual Studio installation and setup environment + $vsWhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe" + $vsPath = & $vsWhere -latest -property installationPath + $vcvarsall = Join-Path $vsPath "VC\Auxiliary\Build\vcvars64.bat" + + foreach ($name in @("llama", "ggml", "mtmd")) { + $dllPath = Join-Path $libDir "$name.dll" + if (Test-Path $dllPath) { + Write-Host "Generating import library for $name.dll" + + # Run dumpbin and lib in a VS Developer environment + $defFile = Join-Path $libDir "$name.def" + $libFile = Join-Path $libDir "$name.lib" + + # Use cmd to run vcvars and then our commands + cmd /c "`"$vcvarsall`" && dumpbin /exports `"$dllPath`" > `"$libDir\${name}_exports.txt`"" + + # Parse exports and create .def file + $exports = Get-Content "$libDir\${name}_exports.txt" + $inExports = $false + $defContent = "LIBRARY $name`r`nEXPORTS`r`n" + + foreach ($line in $exports) { + if ($line -match "^\s+ordinal\s+hint\s+RVA\s+name") { + $inExports = $true + continue + } + if ($inExports -and $line -match "^\s+\d+\s+[0-9A-Fa-f]+\s+[0-9A-Fa-f]+\s+(\S+)") { + $defContent += " $($Matches[1])`r`n" + } + if ($inExports -and $line -match "^\s*$") { + break + } + } + + Set-Content -Path $defFile -Value $defContent + + # Generate .lib from .def + cmd /c "`"$vcvarsall`" && lib /def:`"$defFile`" /machine:x64 /out:`"$libFile`"" + } + } + + Get-ChildItem "$libDir\*.lib" + + - name: Set Windows backend environment + if: matrix.platform.os == 'windows-latest' + shell: powershell + run: echo "LLAMA_BACKEND=vulkan" >> $env:GITHUB_ENV + + - name: Install frontend dependencies + run: npm ci + + - name: Run Rust tests (Linux) + if: matrix.platform.os == 'ubuntu-22.04' + working-directory: src-tauri + env: + LD_LIBRARY_PATH: ${{ github.workspace }}/src-tauri/libs/linux-x64 + run: cargo test --verbose + + - name: Run Rust tests (macOS) + if: matrix.platform.os == 'macos-latest' + working-directory: src-tauri + env: + DYLD_LIBRARY_PATH: ${{ github.workspace }}/src-tauri/libs/darwin-arm64 + run: cargo test --verbose + + - name: Run Rust tests (Windows) + if: matrix.platform.os == 'windows-latest' + working-directory: src-tauri + shell: powershell + run: | + $env:PATH = "${{ github.workspace }}\src-tauri\libs\windows-x64-vulkan;$env:PATH" + cargo test --verbose + + - name: Build frontend + run: npm run build + + - name: Copy libraries for bundling (Linux) + if: matrix.platform.os == 'ubuntu-22.04' + run: | + mkdir -p src-tauri/resources + cp -P src-tauri/libs/linux-x64/* src-tauri/resources/ + + - name: Copy libraries for bundling (Windows) + if: matrix.platform.os == 'windows-latest' + shell: powershell + run: | + New-Item -ItemType Directory -Force -Path src-tauri/resources + Copy-Item -Path src-tauri/libs/windows-x64-vulkan/* -Destination src-tauri/resources/ + + - name: Build Tauri application + run: npm run tauri build + + - name: Upload artifacts (Linux) + if: matrix.platform.os == 'ubuntu-22.04' + uses: actions/upload-artifact@v4 + with: + name: linux-build + path: | + src-tauri/target/release/bundle/deb/*.deb + src-tauri/target/release/bundle/appimage/*.AppImage + if-no-files-found: warn + + - name: Upload artifacts (macOS) + if: matrix.platform.os == 'macos-latest' + uses: actions/upload-artifact@v4 + with: + name: macos-build + path: | + src-tauri/target/release/bundle/dmg/*.dmg + src-tauri/target/release/bundle/macos/*.app + if-no-files-found: warn + + - name: Upload artifacts (Windows) + if: matrix.platform.os == 'windows-latest' + uses: actions/upload-artifact@v4 + with: + name: windows-build + path: | + src-tauri/target/release/bundle/msi/*.msi + src-tauri/target/release/bundle/nsis/*.exe + if-no-files-found: warn diff --git a/src-tauri/build.rs b/src-tauri/build.rs index f036fc8..9ec40e9 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -44,7 +44,12 @@ fn main() { println!("cargo:rustc-link-arg=-Wl,-rpath,@executable_path/../Frameworks"); #[cfg(target_os = "linux")] - println!("cargo:rustc-link-arg=-Wl,-rpath,{}", lib_dir.display()); + { + // Use $ORIGIN to reference the directory containing the executable + // Libraries will be bundled in the same directory as the binary + println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN"); + println!("cargo:rustc-link-arg=-Wl,-rpath,{}", lib_dir.display()); + } // Re-run build script if libraries change println!("cargo:rerun-if-changed=libs/{}", lib_subdir); diff --git a/src-tauri/src/llama_inference.rs b/src-tauri/src/llama_inference.rs index 793fdf3..7dcb8c5 100644 --- a/src-tauri/src/llama_inference.rs +++ b/src-tauri/src/llama_inference.rs @@ -40,18 +40,57 @@ pub struct MtmdInputText { #[repr(C)] pub struct LlamaModelParams { - // Simplified - add fields as needed + // Pointers come first + devices: *mut c_void, + tensor_buft_overrides: *const c_void, + // Then the fields we care about n_gpu_layers: c_int, + split_mode: c_int, + main_gpu: c_int, + tensor_split: *const c_float, + progress_callback: *const c_void, + progress_callback_user_data: *mut c_void, + kv_overrides: *const c_void, + // Booleans at the end + vocab_only: bool, use_mmap: bool, use_mlock: bool, + check_tensors: bool, + // Padding to reach 72 bytes + _padding: [u8; 4], } #[repr(C)] pub struct LlamaContextParams { n_ctx: u32, n_batch: u32, + n_ubatch: u32, + n_seq_max: u32, n_threads: c_int, + n_threads_batch: c_int, + rope_scaling_type: c_int, + pooling_type: c_int, + attention_type: c_int, + flash_attn_type: c_int, + rope_freq_base: c_float, + rope_freq_scale: c_float, + yarn_ext_factor: c_float, + yarn_attn_factor: c_float, + yarn_beta_fast: c_float, + yarn_beta_slow: c_float, + yarn_orig_ctx: u32, + defrag_thold: c_float, + cb_eval: *const c_void, + cb_eval_user_data: *mut c_void, + type_k: c_int, + type_v: c_int, + logits_all: bool, + embeddings: bool, + offload_kqv: bool, flash_attn: bool, + no_perf: bool, + abort_callback: *const c_void, + abort_callback_data: *mut c_void, } #[repr(C)] @@ -59,7 +98,11 @@ pub struct MtmdContextParams { use_gpu: bool, print_timings: bool, n_threads: c_int, + image_marker: *const c_char, media_marker: *const c_char, + flash_attn_type: c_int, + image_min_tokens: c_int, + image_max_tokens: c_int, } pub type LlamaToken = i32; diff --git a/src-tauri/src/model_manager.rs b/src-tauri/src/model_manager.rs index c9bce62..aa73914 100644 --- a/src-tauri/src/model_manager.rs +++ b/src-tauri/src/model_manager.rs @@ -493,8 +493,9 @@ mod tests { let result = ModelManager::get_models_directory(); assert!(result.is_ok()); let path = result.unwrap(); - assert!(path.to_string_lossy().contains("canvas")); - assert!(path.to_string_lossy().contains("models")); + let path_lower = path.to_string_lossy().to_lowercase(); + assert!(path_lower.contains("canvas"), "Path should contain 'canvas': {}", path.display()); + assert!(path_lower.contains("models"), "Path should contain 'models': {}", path.display()); } #[tokio::test] diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 3b88705..671bdf8 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -60,6 +60,19 @@ "libs/darwin-arm64/libmtmd.0.dylib", "libs/darwin-arm64/libggml.0.dylib" ] + }, + "linux": { + "deb": { + "depends": [] + } + }, + "windows": { + "wix": { + "language": "en-US" + }, + "nsis": { + "languages": ["English"] + } } } } diff --git a/src-tauri/tests/contextual_crop_test.rs b/src-tauri/tests/contextual_crop_test.rs index 6ef2a2b..53d85f3 100644 --- a/src-tauri/tests/contextual_crop_test.rs +++ b/src-tauri/tests/contextual_crop_test.rs @@ -22,6 +22,11 @@ fn create_solid_color_image(width: u32, height: u32, r: u8, g: u8, b: u8) -> Vec #[test] fn test_multi_turn_conversation_same_image() { + if std::env::var("CI").is_ok() { + println!("Skipping test in CI - requires SmolVLM model files"); + return; + } + let (model_path, mmproj_path) = get_model_paths(); println!("Loading model..."); @@ -85,6 +90,11 @@ fn test_multi_turn_conversation_same_image() { #[test] fn test_contextual_crop_workflow() { + if std::env::var("CI").is_ok() { + println!("Skipping test in CI - requires SmolVLM model files"); + return; + } + let (model_path, mmproj_path) = get_model_paths(); println!("Loading model for contextual crop test..."); diff --git a/src-tauri/tests/inference_integration_test.rs b/src-tauri/tests/inference_integration_test.rs index 67d63b7..75055ec 100644 --- a/src-tauri/tests/inference_integration_test.rs +++ b/src-tauri/tests/inference_integration_test.rs @@ -2,6 +2,11 @@ use baseweightcanvas_lib::inference_engine::InferenceEngine; #[test] fn test_model_loading() { + if std::env::var("CI").is_ok() { + println!("Skipping test in CI - requires SmolVLM model files"); + return; + } + let model_path = "/home/bowserj/.local/share/canvas/models/smolvlm2-2.2b-instruct/SmolVLM2-2.2B-Instruct-Q4_K_M.gguf"; let mmproj_path = "/home/bowserj/.local/share/canvas/models/smolvlm2-2.2b-instruct/mmproj-SmolVLM2-2.2B-Instruct-f16.gguf"; @@ -14,6 +19,11 @@ fn test_model_loading() { #[test] fn test_inference_with_blue_image() { + if std::env::var("CI").is_ok() { + println!("Skipping test in CI - requires SmolVLM model files"); + return; + } + let model_path = "/home/bowserj/.local/share/canvas/models/smolvlm2-2.2b-instruct/SmolVLM2-2.2B-Instruct-Q4_K_M.gguf"; let mmproj_path = "/home/bowserj/.local/share/canvas/models/smolvlm2-2.2b-instruct/mmproj-SmolVLM2-2.2B-Instruct-f16.gguf"; diff --git a/src-tauri/tests/inference_test.rs b/src-tauri/tests/inference_test.rs index b7703ec..4ac3ba8 100644 --- a/src-tauri/tests/inference_test.rs +++ b/src-tauri/tests/inference_test.rs @@ -17,6 +17,11 @@ fn get_model_paths() -> (String, String) { #[test] fn test_model_loading() { + if std::env::var("CI").is_ok() { + println!("Skipping test in CI - requires SmolVLM model files"); + return; + } + let (model_path, mmproj_path) = get_model_paths(); println!("Loading model from: {}", model_path); @@ -28,6 +33,11 @@ fn test_model_loading() { #[test] fn test_simple_inference() { + if std::env::var("CI").is_ok() { + println!("Skipping test in CI - requires SmolVLM model files"); + return; + } + let (model_path, mmproj_path) = get_model_paths(); println!("Loading model from: {}", model_path); diff --git a/src-tauri/tests/packraft_test.rs b/src-tauri/tests/packraft_test.rs index 9d04660..7a00d21 100644 --- a/src-tauri/tests/packraft_test.rs +++ b/src-tauri/tests/packraft_test.rs @@ -3,6 +3,11 @@ use image::GenericImageView; #[test] fn test_packraft_image_inference() { + if std::env::var("CI").is_ok() { + println!("Skipping test in CI - requires SmolVLM model files"); + return; + } + let model_path = "/home/bowserj/.local/share/canvas/models/smolvlm2-2.2b-instruct/SmolVLM2-2.2B-Instruct-Q4_K_M.gguf"; let mmproj_path = "/home/bowserj/.local/share/canvas/models/smolvlm2-2.2b-instruct/mmproj-SmolVLM2-2.2B-Instruct-f16.gguf"; let image_path = "/home/bowserj/packraft.jpg";