diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..52d385a --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,200 @@ +name: Release + +# Fires when a version tag (e.g. v1.2.3) is pushed — typically right after +# merging the PR that should ship, via `git tag vX.Y.Z && git push origin +# vX.Y.Z` on main. Produces a Windows and a Linux binary archive and +# attaches both to a GitHub Release named after the tag. +on: + push: + tags: + - 'v[0-9]+.[0-9]+.[0-9]+*' + +permissions: + contents: write + +jobs: + # ────────────────────────────────────────────── + # Windows (primary platform) + # ────────────────────────────────────────────── + build-windows: + name: Windows / MSVC 2026 + # Kept in lockstep with ci.yml's build-windows job — see the comment + # there for why this is pinned rather than "windows-latest". + runs-on: windows-2025-vs2026 + + steps: + - uses: actions/checkout@v5 + + - name: Cache Vulkan SDK + id: vulkan-cache + uses: actions/cache@v5 + with: + path: C:\VulkanSDK + key: vulkan-sdk-windows-1.3.290.0 + + - name: Install Vulkan SDK + if: steps.vulkan-cache.outputs.cache-hit != 'true' + shell: pwsh + run: | + $version = "1.3.290.0" + Write-Host "Downloading Vulkan SDK $version..." + Invoke-WebRequest ` + -Uri "https://sdk.lunarg.com/sdk/download/$version/windows/VulkanSDK-$version-Installer.exe" ` + -OutFile "$env:TEMP\VulkanSDK-Installer.exe" + Write-Host "Installing (silent)..." + $p = Start-Process "$env:TEMP\VulkanSDK-Installer.exe" ` + -ArgumentList "--accept-licenses","--accept-messages","--confirm-command","install" ` + -Wait -PassThru + Write-Host "Installer exit code: $($p.ExitCode)" + + - name: Set Vulkan SDK environment + shell: pwsh + run: | + $version = "1.3.290.0" + echo "VULKAN_SDK=C:\VulkanSDK\$version" | Out-File -FilePath $env:GITHUB_ENV -Append + echo "C:\VulkanSDK\$version\Bin" | Out-File -FilePath $env:GITHUB_PATH -Append + + - name: Setup vcpkg + uses: lukka/run-vcpkg@v11 + with: + vcpkgGitCommitId: 57987637aac17d62bac535bc839baca3754490e6 + + - name: Cache vcpkg installed packages + uses: actions/cache@v5 + with: + path: build/vcpkg_installed + # Same key ci.yml uses for this runner image, so a release build + # reuses whatever main's last CI run already populated instead of + # rebuilding every dependency from scratch. + key: vcpkg-windows-2025-vs2026-x64-${{ hashFiles('vcpkg.json') }}-57987637 + + - name: Configure CMake + run: > + cmake -B build + -G "Visual Studio 18 2026" -A x64 + -DCMAKE_TOOLCHAIN_FILE="$env:VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" + + - name: Build + run: cmake --build build --config Release --parallel + + - name: Test + run: ctest --test-dir build -C Release --output-on-failure + + - name: Install + run: cmake --install build --config Release --prefix install + + - name: Bundle runtime DLLs + # vcpkg's default Windows triplet (x64-windows) links dependencies + # dynamically. VCPKG_APPLOCAL_DEPS (on by default with the vcpkg + # toolchain file) already copies the required DLLs next to + # build/Release/chiselcad.exe as a post-build step — `cmake + # --install` only installs the chiselcad target itself, so those + # DLLs have to be copied into the package by hand. + shell: pwsh + run: Copy-Item build\Release\*.dll install\bin\ -ErrorAction SilentlyContinue + + - name: Package + shell: pwsh + run: | + $version = "${{ github.ref_name }}" + Compress-Archive -Path install\* -DestinationPath "chiselcad-$version-windows-x64.zip" + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: chiselcad-windows-x64 + path: chiselcad-*-windows-x64.zip + if-no-files-found: error + + # ────────────────────────────────────────────── + # Linux + # ────────────────────────────────────────────── + build-linux: + name: Linux / GCC + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v5 + + - name: Install Vulkan SDK and build tools + run: | + wget -qO- https://packages.lunarg.com/lunarg-signing-key-pub.asc \ + | sudo tee /etc/apt/trusted.gpg.d/lunarg.asc + sudo wget -qO /etc/apt/sources.list.d/lunarg-vulkan-noble.list \ + https://packages.lunarg.com/vulkan/lunarg-vulkan-noble.list + sudo apt-get update + sudo apt-get install -y \ + vulkan-sdk \ + ninja-build \ + libxinerama-dev \ + libxcursor-dev \ + xorg-dev \ + libglu1-mesa-dev \ + pkg-config + + - name: Setup vcpkg + uses: lukka/run-vcpkg@v11 + with: + vcpkgGitCommitId: 57987637aac17d62bac535bc839baca3754490e6 + + - name: Cache vcpkg installed packages + uses: actions/cache@v5 + with: + path: build/vcpkg_installed + key: vcpkg-linux-x64-${{ hashFiles('vcpkg.json') }}-57987637 + + - name: Configure CMake + run: > + cmake -B build -G Ninja + -DCMAKE_BUILD_TYPE=Release + -DCMAKE_TOOLCHAIN_FILE="$VCPKG_ROOT/scripts/buildsystems/vcpkg.cmake" + + - name: Build + run: cmake --build build --parallel + + - name: Test + run: ctest --test-dir build --output-on-failure + + - name: Install + # vcpkg's default Linux triplet (x64-linux) links dependencies + # statically, so unlike Windows there are no extra shared libraries + # to bundle here — only system/driver libraries (libc, libstdc++, + # the Vulkan loader) are needed, and those are expected to already + # be present on any machine with a working Vulkan driver. + run: cmake --install build --prefix install + + - name: Package + run: | + version="${{ github.ref_name }}" + tar czf "chiselcad-${version}-linux-x64.tar.gz" -C install . + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: chiselcad-linux-x64 + path: chiselcad-*-linux-x64.tar.gz + if-no-files-found: error + + # ────────────────────────────────────────────── + # Publish + # ────────────────────────────────────────────── + publish-release: + name: Publish GitHub Release + needs: [build-windows, build-linux] + runs-on: ubuntu-latest + + steps: + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + path: dist + merge-multiple: true + + - name: Create release + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "${{ github.ref_name }}" dist/* \ + --repo "${{ github.repository }}" \ + --title "${{ github.ref_name }}" \ + --generate-notes diff --git a/CMakeLists.txt b/CMakeLists.txt index a909e9b..be75112 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -75,10 +75,16 @@ if(WIN32) set(PLATFORM_SOURCES src/editor/FileWatcherWin32.cpp ) + set(PLATFORM_UTIL_SOURCES + src/util/ResourcePathsWin32.cpp + ) elseif(UNIX) set(PLATFORM_SOURCES src/editor/FileWatcherInotify.cpp ) + set(PLATFORM_UTIL_SOURCES + src/util/ResourcePathsPosix.cpp + ) endif() # ============================================================ @@ -92,6 +98,10 @@ set(CHISELCAD_SOURCES src/app/Config.cpp src/app/MeshBuilder.cpp + # Util + src/util/ResourcePaths.cpp + ${PLATFORM_UTIL_SOURCES} + # Editor (external editor + file watcher only; no embedded editor in v1) src/editor/ExternalEditor.cpp src/editor/DiagnosticsPanel.cpp @@ -218,6 +228,8 @@ set(CHISELCAD_TEST_SOURCES src/lang/Interpreter.cpp src/lang/SourceLoader.cpp src/csg/CsgEvaluator.cpp + src/util/ResourcePaths.cpp + ${PLATFORM_UTIL_SOURCES} src/import/StlLoader.cpp src/import/OffLoader.cpp src/import/DxfLoader.cpp diff --git a/src/app/Application.cpp b/src/app/Application.cpp index bf3f4ca..de0de94 100644 --- a/src/app/Application.cpp +++ b/src/app/Application.cpp @@ -21,6 +21,7 @@ #endif #include "editor/ExternalEditor.h" +#include "util/ResourcePaths.h" namespace chisel::app { using namespace chisel::io; @@ -249,7 +250,8 @@ void Application::initVulkan() { glfwGetFramebufferSize(m_window, &w, &h); m_vma.init(m_ctx.instance(), m_ctx.physDevice(), m_ctx.device()); m_swapchain.init(m_ctx, static_cast(w), static_cast(h)); - m_pipeline.init(m_ctx.device(), m_swapchain.renderPass(), CHISELCAD_SHADER_DIR, + m_pipeline.init(m_ctx.device(), m_swapchain.renderPass(), + chisel::util::resolveShaderDir().string(), m_ctx.fillModeNonSolidSupported()); m_renderer.init(m_ctx, m_swapchain); } diff --git a/src/csg/CsgEvaluator.cpp b/src/csg/CsgEvaluator.cpp index 319ee29..13c1391 100644 --- a/src/csg/CsgEvaluator.cpp +++ b/src/csg/CsgEvaluator.cpp @@ -10,6 +10,7 @@ #include "import/ThreeMfLoader.h" #include "util/PathSuffix.h" #include "util/PathUtf8.h" +#include "util/ResourcePaths.h" #include #include @@ -17,10 +18,6 @@ #include #include -#ifndef CHISELCAD_RESOURCE_DIR -#error "CHISELCAD_RESOURCE_DIR must be defined by the build (see CMakeLists.txt)" -#endif - namespace chisel::csg { using namespace chisel::lang; @@ -31,7 +28,7 @@ static constexpr double kDeg2Rad = 3.14159265358979323846 / 180.0; // Not baseDir-relative — this is a build-time resource path, not a // .scad-file-relative user path (see resolveFilePathArg()). static std::filesystem::path defaultFontPath() { - return std::filesystem::path(CHISELCAD_RESOURCE_DIR) / "fonts" / "Roboto-Regular.ttf"; + return chisel::util::resolveResourceDir() / "fonts" / "Roboto-Regular.ttf"; } // --------------------------------------------------------------------------- diff --git a/src/render/Pipeline.cpp b/src/render/Pipeline.cpp index 5078a8c..674c8ed 100644 --- a/src/render/Pipeline.cpp +++ b/src/render/Pipeline.cpp @@ -1,6 +1,7 @@ #include "Pipeline.h" #include "GpuMesh.h" #include +#include #include #include #include @@ -229,6 +230,12 @@ VkPipeline Pipeline::buildBackgroundPipeline(VkDevice device, VkRenderPass rende // --------------------------------------------------------------------------- // init // --------------------------------------------------------------------------- +namespace { +std::string shaderPath(const std::string& shaderDir, const char* name) { + return (std::filesystem::path(shaderDir) / name).string(); +} +} // namespace + void Pipeline::init(VkDevice device, VkRenderPass renderPass, const std::string& shaderDir, bool wireSupported) { @@ -247,8 +254,8 @@ void Pipeline::init(VkDevice device, VkRenderPass renderPass, layoutCI.pPushConstantRanges = pcRanges.data(); VK_CHECK(vkCreatePipelineLayout(device, &layoutCI, nullptr, &m_layout)); - auto vertMod = loadShader(device, shaderDir + "mesh.vert.spv"); - auto fragMod = loadShader(device, shaderDir + "mesh.frag.spv"); + auto vertMod = loadShader(device, shaderPath(shaderDir, "mesh.vert.spv")); + auto fragMod = loadShader(device, shaderPath(shaderDir, "mesh.frag.spv")); // Solid pipeline: filled triangles, back-face culling, no depth bias m_solidPipeline = buildPipeline(device, renderPass, vertMod, fragMod, @@ -270,8 +277,8 @@ void Pipeline::init(VkDevice device, VkRenderPass renderPass, bgLayoutCI.sType = VK_STRUCTURE_TYPE_PIPELINE_LAYOUT_CREATE_INFO; VK_CHECK(vkCreatePipelineLayout(device, &bgLayoutCI, nullptr, &m_bgLayout)); - auto bgVert = loadShader(device, shaderDir + "background.vert.spv"); - auto bgFrag = loadShader(device, shaderDir + "background.frag.spv"); + auto bgVert = loadShader(device, shaderPath(shaderDir, "background.vert.spv")); + auto bgFrag = loadShader(device, shaderPath(shaderDir, "background.frag.spv")); m_bgPipeline = buildBackgroundPipeline(device, renderPass, bgVert, bgFrag); vkDestroyShaderModule(device, bgVert, nullptr); vkDestroyShaderModule(device, bgFrag, nullptr); diff --git a/src/util/ResourcePaths.cpp b/src/util/ResourcePaths.cpp new file mode 100644 index 0000000..fddda2e --- /dev/null +++ b/src/util/ResourcePaths.cpp @@ -0,0 +1,41 @@ +#include "util/ResourcePaths.h" + +#include + +namespace chisel::util { + +namespace { + +std::filesystem::path installedCandidate(const char* leaf) { + return executableDir() / ".." / "share" / "chiselcad" / leaf; +} + +} // namespace + +std::filesystem::path resolveShaderDir() { + std::filesystem::path installed = installedCandidate("shaders"); + std::error_code ec; + if (std::filesystem::exists(installed, ec)) { + return installed; + } +#ifdef CHISELCAD_SHADER_DIR + return CHISELCAD_SHADER_DIR; +#else + return installed; +#endif +} + +std::filesystem::path resolveResourceDir() { + std::filesystem::path installed = installedCandidate("resources"); + std::error_code ec; + if (std::filesystem::exists(installed, ec)) { + return installed; + } +#ifdef CHISELCAD_RESOURCE_DIR + return CHISELCAD_RESOURCE_DIR; +#else + return installed; +#endif +} + +} // namespace chisel::util diff --git a/src/util/ResourcePaths.h b/src/util/ResourcePaths.h new file mode 100644 index 0000000..bdebeef --- /dev/null +++ b/src/util/ResourcePaths.h @@ -0,0 +1,23 @@ +#pragma once +#include + +namespace chisel::util { + +// Directory containing the currently running executable, resolved via a +// platform API (never argv[0] or the current working directory, both of +// which are caller-controlled and unreliable). +std::filesystem::path executableDir(); + +// Resolves the directory holding compiled SPIR-V shaders. Prefers the +// relocatable installed layout (/../share/chiselcad/shaders, as +// produced by `cmake --install`) so a packaged release binary keeps working +// after being copied to another machine; falls back to the compile-time +// build-tree path (CHISELCAD_SHADER_DIR) for dev builds run in place from +// the build directory. +std::filesystem::path resolveShaderDir(); + +// Same idea as resolveShaderDir(), for the bundled resources/ directory +// (fonts, etc). +std::filesystem::path resolveResourceDir(); + +} // namespace chisel::util diff --git a/src/util/ResourcePathsPosix.cpp b/src/util/ResourcePathsPosix.cpp new file mode 100644 index 0000000..3621118 --- /dev/null +++ b/src/util/ResourcePathsPosix.cpp @@ -0,0 +1,20 @@ +#if defined(__linux__) +#include "util/ResourcePaths.h" + +#include +#include + +namespace chisel::util { + +std::filesystem::path executableDir() { + char buf[PATH_MAX]; + ssize_t len = readlink("/proc/self/exe", buf, sizeof(buf) - 1); + if (len <= 0) { + return std::filesystem::current_path(); + } + buf[len] = '\0'; + return std::filesystem::path(buf).parent_path(); +} + +} // namespace chisel::util +#endif diff --git a/src/util/ResourcePathsWin32.cpp b/src/util/ResourcePathsWin32.cpp new file mode 100644 index 0000000..19960fe --- /dev/null +++ b/src/util/ResourcePathsWin32.cpp @@ -0,0 +1,20 @@ +#if defined(_WIN32) +#include "util/ResourcePaths.h" + +#define WIN32_LEAN_AND_MEAN +#define NOMINMAX +#include + +namespace chisel::util { + +std::filesystem::path executableDir() { + wchar_t buf[MAX_PATH]; + DWORD len = GetModuleFileNameW(nullptr, buf, MAX_PATH); + if (len == 0 || len == MAX_PATH) { + return std::filesystem::current_path(); + } + return std::filesystem::path(buf, buf + len).parent_path(); +} + +} // namespace chisel::util +#endif