From de067ef32e883fe7ab28468824f0e1f7729c78b8 Mon Sep 17 00:00:00 2001 From: Mitchell Date: Tue, 10 Feb 2026 23:23:42 -0600 Subject: [PATCH 1/4] ci: add github actions config closes #14 --- .github/workflows/ci.yml | 39 +++++++++++++ .github/workflows/release.yml | 107 ++++++++++++++++++++++++++++++++++ build.zig.zon | 13 +++-- 3 files changed, 153 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..f9eec03 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: CI + +on: + pull_request: + types: + - opened + - synchronize + - reopened + - ready_for_review + +permissions: + contents: read + id-token: write + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-and-test: + name: Build and Test + if: github.event.pull_request.draft == false + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + + - name: Enable Nix Cache + uses: DeterminateSystems/magic-nix-cache-action@main + + - name: Build + run: nix develop -c zig build -Dnix + + - name: Test + run: nix develop -c zig build test -Dnix diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..cfd1049 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,107 @@ +name: Release + +on: + push: + branches: + - main + - dev-14 + tags: + - "v*" + workflow_dispatch: + +permissions: + contents: write + id-token: write + +jobs: + release: + name: Release + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + + - name: Enable Nix Cache + uses: DeterminateSystems/magic-nix-cache-action@main + + - name: Build and Test + run: | + nix develop -c bash -lc ' + set -euo pipefail + zig build -Dnix + zig build test -Dnix + ' + + - name: Compute Release Tag + id: release_tag + run: | + version="$(sed -n 's/^[[:space:]]*\.version = "\(.*\)",/\1/p' build.zig.zon | head -n1)" + if [ -z "$version" ]; then + echo "Failed to parse version from build.zig.zon" >&2 + exit 1 + fi + short_sha="$(git rev-parse --short=8 HEAD)" + echo "tag=v${version}-${short_sha}" >> "$GITHUB_OUTPUT" + + - name: Build Release Binaries + run: nix develop -c zig build -Dnix -Doptimize=ReleaseFast + + - name: Package Linux + run: tar -czf spacecap-linux-x86_64.tar.gz -C zig-out linux + + - name: Package Windows + run: zip -r spacecap-windows-x86_64.zip zig-out/windows + + - name: Generate Checksums + run: | + sha256sum spacecap-linux-x86_64.tar.gz > SHA256SUMS.txt + sha256sum spacecap-windows-x86_64.zip >> SHA256SUMS.txt + + - name: Generate Commit Notes + id: commit_notes + run: | + previous_tag="$(git describe --tags --abbrev=0 2>/dev/null || true)" + if [ -n "$previous_tag" ]; then + commit_range="${previous_tag}..HEAD" + else + commit_range="HEAD" + fi + + { + echo "body<> "$GITHUB_OUTPUT" + + - name: Upload Workflow Artifacts + uses: actions/upload-artifact@v4 + with: + name: spacecap-release-assets + path: | + spacecap-linux-x86_64.tar.gz + spacecap-windows-x86_64.zip + SHA256SUMS.txt + + - name: Publish GitHub Release Assets + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.release_tag.outputs.tag }} + target_commitish: ${{ github.sha }} + generate_release_notes: true + append_body: true + body: ${{ steps.commit_notes.outputs.body }} + files: | + spacecap-linux-x86_64.tar.gz + spacecap-windows-x86_64.zip + SHA256SUMS.txt diff --git a/build.zig.zon b/build.zig.zon index 6fa8606..5b9ffb9 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -74,8 +74,9 @@ .hash = "N-V-__8AAPJS7wFRVAIGhMZ7cis5e-y5LZfn2KfO5O2jiHGH", }, .vulkan_zig = .{ - .url = "https://github.com/Snektron/vulkan-zig/archive/zig-0.15-compat.tar.gz", - .hash = "vulkan-0.0.0-r7Ytx_VDAwAiMl0YSu2UOkVMIGJN7CeIQaxJR-hUSfD6", + // Pinned to 0.15.1 + .url = "https://github.com/Snektron/vulkan-zig/archive/af34c77ab52e8ce353faf2cd13c1ef13a99c2171.tar.gz", + .hash = "vulkan-0.0.0-r7YtxztIAwBc30xIMx4tzfUcEmc7goWiFJkR13yeLfi8", }, .imguiz = .{ .url = "git+https://github.com/mgerb/imguiz#7d6e1a4dfb2f31da24c9f76e0c4ba8285e89d289", @@ -86,12 +87,12 @@ .hash = "N-V-__8AAL-l8gSpW3fjjSbxZmbJt3OvF_ofxYzT8TsfzD9T", }, .ffmpeg_linux = .{ - .url = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n7.1-latest-linux64-gpl-shared-7.1.tar.xz", - .hash = "N-V-__8AAMVhFwyEn-loOh3RvLuNHFbK-_cUx6F781wmKELD", + .url = "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-02-10-13-08/ffmpeg-n7.1.3-40-gcddd06f3b9-linux64-gpl-shared-7.1.tar.xz", + .hash = "N-V-__8AAMWBGwzjWy3ngwsCARDuxQb9JkPi8B5Qwjhi8Fvk", }, .ffmpeg_windows = .{ - .url = "https://github.com/BtbN/FFmpeg-Builds/releases/download/latest/ffmpeg-n7.1-latest-win64-gpl-shared-7.1.zip", - .hash = "N-V-__8AAJk_Dgsw1bGPOwZaDiVZlSS6gFZfdqjg2ObF4vOu", + .url = "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-02-10-13-08/ffmpeg-n7.1.3-40-gcddd06f3b9-win64-gpl-shared-7.1.zip", + .hash = "N-V-__8AAJl9FQsCnNsH8YA5Qbdo9lCRXd8h-ZU9O0Uq5BH7", }, .libportal = .{ .url = "https://github.com/flatpak/libportal/archive/refs/tags/0.9.1.tar.gz", From 15827502f494f8a2f072a0e47780968b8d9c4d7e Mon Sep 17 00:00:00 2001 From: Mitchell Date: Wed, 11 Feb 2026 02:00:56 -0600 Subject: [PATCH 2/4] build: build appimage --- .github/workflows/release.yml | 106 +++++++++-------- README.md | 60 +++++++++- build.zig | 191 +++++++++++++++++++++++-------- build.zig.zon | 2 +- build_app_image.sh | 20 ++++ flake.nix | 79 +++++++++++-- packaging/linux/spacecap.desktop | 8 ++ packaging/linux/spacecap.svg | 10 ++ src/vulkan/vulkan.zig | 3 +- 9 files changed, 364 insertions(+), 115 deletions(-) create mode 100755 build_app_image.sh create mode 100644 packaging/linux/spacecap.desktop create mode 100644 packaging/linux/spacecap.svg diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cfd1049..e624e0a 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,7 +7,6 @@ on: - dev-14 tags: - "v*" - workflow_dispatch: permissions: contents: write @@ -21,6 +20,8 @@ jobs: steps: - name: Checkout uses: actions/checkout@v4 + with: + fetch-depth: 0 - name: Install Nix uses: DeterminateSystems/nix-installer-action@main @@ -28,80 +29,87 @@ jobs: - name: Enable Nix Cache uses: DeterminateSystems/magic-nix-cache-action@main + - name: Resolve and Validate Release Version + id: version + run: | + base_version="$(sed -n 's/^[[:space:]]*\.version = "\(.*\)",/\1/p' build.zig.zon | head -n1)" + if [ -z "$base_version" ]; then + echo "Failed to parse version from build.zig.zon" >&2 + exit 1 + fi + + if [ "${GITHUB_REF_TYPE}" = "tag" ]; then + expected_tag="v${base_version}" + actual_tag="${GITHUB_REF_NAME}" + + if [ "$actual_tag" != "$expected_tag" ]; then + echo "Tag/version mismatch. build.zig.zon has ${base_version}, expected tag ${expected_tag}, got ${actual_tag}" >&2 + exit 1 + fi + + release_tag="${actual_tag}" + release_version="${base_version}" + prerelease="false" + else + short_sha="$(git rev-parse --short=8 HEAD)" + release_version="${base_version}+g${short_sha}" + release_tag="nightly" + prerelease="true" + fi + + echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT" + echo "release_version=${release_version}" >> "$GITHUB_OUTPUT" + echo "prerelease=${prerelease}" >> "$GITHUB_OUTPUT" + - name: Build and Test run: | nix develop -c bash -lc ' set -euo pipefail + echo "Release version: ${{ steps.version.outputs.release_version }}" zig build -Dnix zig build test -Dnix ' - - name: Compute Release Tag - id: release_tag - run: | - version="$(sed -n 's/^[[:space:]]*\.version = "\(.*\)",/\1/p' build.zig.zon | head -n1)" - if [ -z "$version" ]; then - echo "Failed to parse version from build.zig.zon" >&2 - exit 1 - fi - short_sha="$(git rev-parse --short=8 HEAD)" - echo "tag=v${version}-${short_sha}" >> "$GITHUB_OUTPUT" - - name: Build Release Binaries - run: nix develop -c zig build -Dnix -Doptimize=ReleaseFast + run: nix develop -c zig build -Dappimage -Doptimize=ReleaseFast - - name: Package Linux - run: tar -czf spacecap-linux-x86_64.tar.gz -C zig-out linux + # - name: Package Linux + # run: tar -czf spacecap-linux-x86_64.tar.gz -C zig-out linux - - name: Package Windows - run: zip -r spacecap-windows-x86_64.zip zig-out/windows + # - name: Package Windows + # run: zip -r spacecap-windows-x86_64.zip zig-out/windows - name: Generate Checksums run: | - sha256sum spacecap-linux-x86_64.tar.gz > SHA256SUMS.txt - sha256sum spacecap-windows-x86_64.zip >> SHA256SUMS.txt - - - name: Generate Commit Notes - id: commit_notes - run: | - previous_tag="$(git describe --tags --abbrev=0 2>/dev/null || true)" - if [ -n "$previous_tag" ]; then - commit_range="${previous_tag}..HEAD" - else - commit_range="HEAD" - fi - - { - echo "body<> "$GITHUB_OUTPUT" + # sha256sum spacecap-linux-x86_64.tar.gz > SHA256SUMS.txt + # sha256sum spacecap-windows-x86_64.zip >> SHA256SUMS.txt + sha256sum zig-out/linux/spacecap-linux-x86_64.AppImage >> SHA256SUMS.txt - name: Upload Workflow Artifacts uses: actions/upload-artifact@v4 with: name: spacecap-release-assets + # spacecap-linux-x86_64.tar.gz is currently disabled. + # spacecap-windows-x86_64.zip is currently disabled. path: | - spacecap-linux-x86_64.tar.gz - spacecap-windows-x86_64.zip + zig-out/linux/spacecap-linux-x86_64.AppImage SHA256SUMS.txt + - name: Move Nightly Tag + if: github.ref_type != 'tag' + run: | + git tag -f nightly "${GITHUB_SHA}" + git push --force origin refs/tags/nightly + - name: Publish GitHub Release Assets uses: softprops/action-gh-release@v2 with: - tag_name: ${{ steps.release_tag.outputs.tag }} + tag_name: ${{ steps.version.outputs.release_tag }} target_commitish: ${{ github.sha }} + prerelease: ${{ steps.version.outputs.prerelease }} generate_release_notes: true - append_body: true - body: ${{ steps.commit_notes.outputs.body }} + # spacecap-linux-x86_64.tar.gz is currently disabled. + # spacecap-windows-x86_64.zip is currently disabled. files: | - spacecap-linux-x86_64.tar.gz - spacecap-windows-x86_64.zip + zig-out/linux/spacecap-linux-x86_64.AppImage SHA256SUMS.txt diff --git a/README.md b/README.md index be62331..a040829 100644 --- a/README.md +++ b/README.md @@ -27,10 +27,64 @@ alternative to OBS for capturing video replays. **NOTE:** I'm testing with an RTX 3080 GPU. I have no idea if AMD works. I don't have one to test on. -## How to compile and run +## Linux Requirements -Currently this only works on Linux with [Nix](https://nixos.org/download/#download-nix). -A GPU that supports Vulkan Video is required for recording. +- vulkan (and related graphics drivers) +- pipewire +- pipewire-pulse + +### Installation + +**NOTE:** A reboot may be required. + +#### NixOS + +There is currently not a Nix package published, but for now you can use +`appimage-run` to execute the appimage. + +TODO: Add vulkan instructions. + +```nix +{...}: { + security.rtkit.enable = true; + + services.pipewire = { + enable = true; + alsa.enable = false; + pulse.enable = true; + wireplumber.enable = true; + }; + + environment.systemPackages = with pkgs; [ + appimage-run + ]; +} +``` + +#### Arch + +TODO: Add vulkan instructions. + +```sh +sudo pacman -S --needed wireplumber pipewire pipewire-pulse pipewire-alsa fuse3 +systemctl --user --now enable pipewire pipewire-pulse wireplumber +``` + +#### Ubuntu + +TODO: Add vulkan instructions. + +```sh +sudo apt update +sudo apt install wireplumber pipewire pipewire-pulse pipewire-alsa fuse3 +systemctl --user --now enable pipewire pipewire-pulse wireplumber +``` + +## Development + +[Nix](https://nixos.org/download/#download-nix) is required for development, +unless you want to install all dependencies manually. See `flake.nix` if you'd +like to do so. ```sh # Build diff --git a/build.zig b/build.zig index efbc231..f97a754 100644 --- a/build.zig +++ b/build.zig @@ -89,11 +89,7 @@ fn addLinuxDependencies( target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, ) !void { - // xkbcommon - // NOTE: may not be used actually? - // exe.addLibraryPath(.{ .cwd_relative = std.posix.getenv("LIBXKBCOMMON").? }); - // try installAndLinkSystemLibrary(allocator, b, exe, std.posix.getenv("LIBXKBCOMMON").?, "xkbcommon", .linux); - + _ = allocator; const pipewire = b.dependency("pipewire", .{ .optimize = optimize, .target = target, @@ -107,24 +103,25 @@ fn addLinuxDependencies( exe.root_module.addImport("gio", gobject.module("gio2")); exe.root_module.addImport("gobject", gobject.module("gobject2")); - // libportal - try installAndLinkSystemLibrary(allocator, b, exe, std.posix.getenv("LIBPORTAL").?, "portal", .linux, "libportal.so"); + exe.root_module.linkSystemLibrary("glib-2.0", .{}); + exe.root_module.linkSystemLibrary("gio-2.0", .{}); + exe.root_module.linkSystemLibrary("gobject-2.0", .{}); + exe.root_module.linkSystemLibrary("portal", .{}); - // vulkan - exe.addLibraryPath(.{ .cwd_relative = std.posix.getenv("VULKAN_SDK_PATH").? }); - try installAndLinkSystemLibrary(allocator, b, exe, std.posix.getenv("VULKAN_SDK_PATH").?, "vulkan", .linux, "libvulkan.so.1"); + // Vulkan is linked directly, because it is required that the + // system has the libs installed. + exe.root_module.linkSystemLibrary("vulkan", .{}); - // ffmpeg + // TODO: Statically link ffmpeg with the zig version. const ffmpeg_linux = b.dependency("ffmpeg_linux", .{}); exe.addLibraryPath(ffmpeg_linux.path("lib")); - const ffmpeg_path = ffmpeg_linux.path("lib").getPath(b); - - try installAndLinkSystemLibrary(allocator, b, exe, ffmpeg_path, "avformat", .linux, "libavformat.so.61"); - try installAndLinkSystemLibrary(allocator, b, exe, ffmpeg_path, "avcodec", .linux, "libavcodec.so.61"); - try installAndLinkSystemLibrary(allocator, b, exe, ffmpeg_path, "avdevice", .linux, "libavdevice.so.61"); - try installAndLinkSystemLibrary(allocator, b, exe, ffmpeg_path, "avfilter", .linux, "libavfilter.so.10"); - try installAndLinkSystemLibrary(allocator, b, exe, ffmpeg_path, "avutil", .linux, "libavutil.so.59"); - try installAndLinkSystemLibrary(allocator, b, exe, ffmpeg_path, "swresample", .linux, "libswresample.so.5"); + + exe.root_module.linkSystemLibrary("avformat", .{}); + exe.root_module.linkSystemLibrary("avcodec", .{}); + exe.root_module.linkSystemLibrary("avdevice", .{}); + exe.root_module.linkSystemLibrary("avfilter", .{}); + exe.root_module.linkSystemLibrary("avutil", .{}); + exe.root_module.linkSystemLibrary("swresample", .{}); } /// Install a dynamic library in the /lib directory @@ -132,40 +129,41 @@ fn addLinuxDependencies( /// /// Lib name should be the name of the lib without extensions /// e.g. avformat NOT libavformat.so -fn installAndLinkSystemLibrary( +fn installAndLinkSystemLibrary(args: struct { allocator: std.mem.Allocator, b: *std.Build, exe: *std.Build.Step.Compile, source_dir: []const u8, lib_name: []const u8, target: enum { linux, windows }, - file_name_override: ?[]const u8, -) !void { - const file_name = file_name_override orelse switch (target) { - .linux => try std.fmt.allocPrint(allocator, "lib{s}.so", .{lib_name}), - .windows => try std.fmt.allocPrint(allocator, "{s}.dll", .{lib_name}), + file_name_override: ?[]const u8 = null, + link_options: std.Build.Module.LinkSystemLibraryOptions = .{}, +}) !void { + const file_name = args.file_name_override orelse switch (args.target) { + .linux => try std.fmt.allocPrint(args.allocator, "lib{s}.so", .{args.lib_name}), + .windows => try std.fmt.allocPrint(args.allocator, "{s}.dll", .{args.lib_name}), }; defer { - if (file_name_override == null) { - allocator.free(file_name); + if (args.file_name_override == null) { + args.allocator.free(file_name); } } - const full_file_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ source_dir, file_name }); - defer allocator.free(full_file_path); + const full_file_path = try std.fmt.allocPrint(args.allocator, "{s}/{s}", .{ args.source_dir, file_name }); + defer args.allocator.free(full_file_path); - const target_name = switch (target) { + const target_name = switch (args.target) { .linux => "linux/lib", .windows => "windows/lib", }; - const dest_path = try std.fmt.allocPrint(allocator, "{s}/{s}", .{ target_name, file_name }); - defer allocator.free(dest_path); + const dest_path = try std.fmt.allocPrint(args.allocator, "{s}/{s}", .{ target_name, file_name }); + defer args.allocator.free(dest_path); - const step = b.addInstallFile(.{ .cwd_relative = full_file_path }, dest_path); - exe.step.dependOn(&step.step); + const step = args.b.addInstallFile(.{ .cwd_relative = full_file_path }, dest_path); + args.exe.step.dependOn(&step.step); - exe.linkSystemLibrary(lib_name); + args.exe.root_module.linkSystemLibrary(args.lib_name, args.link_options); } fn buildWindows( @@ -197,7 +195,14 @@ fn buildWindows( exe.addLibraryPath(.{ .cwd_relative = std.posix.getenv("VULKAN_SDK_PATH_WINDOWS").? }); - try installAndLinkSystemLibrary(allocator, b, exe, std.posix.getenv("VULKAN_SDK_PATH_WINDOWS").?, "vulkan-1", .windows, null); + try installAndLinkSystemLibrary(.{ + .allocator = allocator, + .b = b, + .exe = exe, + .source_dir = std.posix.getenv("VULKAN_SDK_PATH_WINDOWS").?, + .lib_name = "vulkan-1", + .target = .windows, + }); // All windows machines should be able to link to this by default exe.linkSystemLibrary("gdi32"); @@ -210,11 +215,46 @@ fn buildWindows( exe.addLibraryPath(ffmpeg_windows.path("bin")); const ffmpeg_path = ffmpeg_windows.path("bin").getPath(b); - try installAndLinkSystemLibrary(allocator, b, exe, ffmpeg_path, "avformat-61", .windows, null); - try installAndLinkSystemLibrary(allocator, b, exe, ffmpeg_path, "avcodec-61", .windows, null); - try installAndLinkSystemLibrary(allocator, b, exe, ffmpeg_path, "avdevice-61", .windows, null); - try installAndLinkSystemLibrary(allocator, b, exe, ffmpeg_path, "avfilter-10", .windows, null); - try installAndLinkSystemLibrary(allocator, b, exe, ffmpeg_path, "avutil-59", .windows, null); + try installAndLinkSystemLibrary(.{ + .allocator = allocator, + .b = b, + .exe = exe, + .source_dir = ffmpeg_path, + .lib_name = "avformat-61", + .target = .windows, + }); + try installAndLinkSystemLibrary(.{ + .allocator = allocator, + .b = b, + .exe = exe, + .source_dir = ffmpeg_path, + .lib_name = "avcodec-61", + .target = .windows, + }); + try installAndLinkSystemLibrary(.{ + .allocator = allocator, + .b = b, + .exe = exe, + .source_dir = ffmpeg_path, + .lib_name = "avdevice-61", + .target = .windows, + }); + try installAndLinkSystemLibrary(.{ + .allocator = allocator, + .b = b, + .exe = exe, + .source_dir = ffmpeg_path, + .lib_name = "avfilter-10", + .target = .windows, + }); + try installAndLinkSystemLibrary(.{ + .allocator = allocator, + .b = b, + .exe = exe, + .source_dir = ffmpeg_path, + .lib_name = "avutil-59", + .target = .windows, + }); const install_step = b.addInstallArtifact(exe, .{ .dest_dir = .{ .override = .{ .custom = "windows" } }, @@ -227,7 +267,8 @@ fn buildLinux( b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, -) !void { + nix: bool, +) !*std.Build.Step { const module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), .target = target, @@ -242,7 +283,14 @@ fn buildLinux( // with pipewire using the Zig backend. Just stick to LLVM for now... .use_llvm = true, }); - exe.addRPath(b.path("./lib")); + + if (!nix) { + // This prevents linker errors when building for generic Linux target on NixOS. + exe.linker_allow_shlib_undefined = true; + // NixOS can't run dynamically linked executables, so there + // is no need to change the rpath. + exe.root_module.addRPathSpecial("$ORIGIN/lib"); + } try addSharedDependencies(allocator, b, exe, target, optimize); try addLinuxDependencies(allocator, b, exe, target, optimize); @@ -252,7 +300,9 @@ fn buildLinux( }); b.getInstallStep().dependOn(&install_step.step); - const run_cmd = b.addRunArtifact(exe); + const run_cmd = b.addSystemCommand(&.{ + b.getInstallPath(.prefix, "linux/" ++ EXE_NAME), + }); run_cmd.step.dependOn(b.getInstallStep()); if (b.args) |args| { @@ -261,6 +311,31 @@ fn buildLinux( const run_step = b.step("run", "Run the app"); run_step.dependOn(&run_cmd.step); + + return &install_step.step; +} + +fn buildLinuxAppImage( + b: *std.Build, + allocator: std.mem.Allocator, + linux_install_step: *std.Build.Step, +) *std.Build.Step { + const appimage_step = b.step("appimage", "Build Linux AppImage"); + + const file = std.fs.cwd().openFile("./build_app_image.sh", .{ .mode = .read_only }) catch unreachable; + defer file.close(); + const stat = file.stat() catch unreachable; + + var reader = file.reader(&.{}); + const buffer = reader.interface.readAlloc(allocator, stat.size) catch unreachable; + defer allocator.free(buffer); + + const cmd = b.addSystemCommand(&.{ "bash", "-lc", buffer }); + + cmd.step.dependOn(linux_install_step); + appimage_step.dependOn(&cmd.step); + + return appimage_step; } fn buildUnitTestsDefault( @@ -308,20 +383,40 @@ pub fn build(b: *std.Build) !void { defer _ = gpa.deinit(); const allocator = gpa.allocator(); - const nix = b.option(bool, "nix", "If on NixOS, use this flag to run"); + const nix = b.option(bool, "nix", "If on NixOS, use this flag to run") orelse false; + const appimage = b.option(bool, "appimage", "Build Linux AppImage after install") orelse false; + + if (appimage and nix == true) { + std.log.err("AppImage builds require generic linux target. Run without -Dnix.", .{}); + return error.InvalidBuildConfig; + } const optimize = b.standardOptimizeOption(.{}); + if (appimage and optimize == .Debug) { + std.log.err("AppImage builds require a release optimize mode. Use -Doptimize=ReleaseFast, -Doptimize=ReleaseSafe, or -Doptimize=ReleaseSmall.", .{}); + return error.InvalidBuildConfig; + } try buildWindows(allocator, b, optimize); - // TODO: Linux build is currently broken due to llvm linker errors. Check back - // when switched back to zig linker when it's fixed. const target = if (nix == true) b.standardTargetOptions(.{}) else b.resolveTargetQuery(.{ .os_tag = .linux, .abi = .gnu, .cpu_arch = .x86_64, }); - try buildLinux(allocator, b, target, optimize); + const linux_install_step = try buildLinux( + allocator, + b, + target, + optimize, + nix, + ); + const appimage_step = buildLinuxAppImage(b, allocator, linux_install_step); + + if (appimage) { + b.getInstallStep().dependOn(appimage_step); + } + try buildUnitTestsDefault(allocator, b, target, optimize); } diff --git a/build.zig.zon b/build.zig.zon index 5b9ffb9..3611d4d 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -10,7 +10,7 @@ // This is a [Semantic Version](https://semver.org/). // In a future version of Zig it will be used for package deduplication. - .version = "0.0.0", + .version = "0.1.1", // Together with name, this represents a globally unique package // identifier. This field is generated by the Zig toolchain when the diff --git a/build_app_image.sh b/build_app_image.sh new file mode 100755 index 0000000..26324a0 --- /dev/null +++ b/build_app_image.sh @@ -0,0 +1,20 @@ +# Requires appimagetool and linuxdeploy. + +set -euo pipefail + +# Get page-aligned errors on some dynamic libs without this. +export NO_STRIP=1 + +rm -rf AppDir +rm -f zig-out/linux/spacecap-linux-x86_64.AppImage + +# NOTE: Vulkan is excluded because system libraries should be used. +LD_LIBRARY_PATH="zig-out/linux/lib:${LD_LIBRARY_PATH:-}" linuxdeploy \ + --appdir AppDir \ + --executable zig-out/linux/spacecap \ + --desktop-file packaging/linux/spacecap.desktop \ + --icon-file packaging/linux/spacecap.svg \ + --exclude-library libvulkan.so.1 + +env -u SOURCE_DATE_EPOCH APPIMAGE_EXTRACT_AND_RUN=1 ARCH=x86_64 appimagetool AppDir zig-out/linux/spacecap-linux-x86_64.AppImage +rm -rf AppDir diff --git a/flake.nix b/flake.nix index 368311f..b94e6ef 100644 --- a/flake.nix +++ b/flake.nix @@ -36,20 +36,76 @@ mv zls $out/bin/ ''; }; + linuxdeploy = pkgs.stdenv.mkDerivation { + pname = "linuxdeploy"; + version = "continuous"; + src = pkgs.fetchurl { + url = "https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"; + sha256 = "sha256-nFCMLLcA+ExmAufWDpnxgGodn5Doomw8nrvHxiu5UFs="; + }; + dontUnpack = true; + dontFixup = true; + dontStrip = true; + installPhase = '' + mkdir -p $out/bin $out/libexec + cp "$src" "$out/libexec/linuxdeploy-x86_64.AppImage" + chmod +x "$out/libexec/linuxdeploy-x86_64.AppImage" + cat > "$out/bin/linuxdeploy" < "$out/bin/appimagetool" < + + + + + + + + + diff --git a/src/vulkan/vulkan.zig b/src/vulkan/vulkan.zig index 73ad757..1f12c91 100644 --- a/src/vulkan/vulkan.zig +++ b/src/vulkan/vulkan.zig @@ -20,8 +20,7 @@ pub const Device = vk.DeviceProxy; pub const CommandBuffer = vk.CommandBufferProxy; pub const API_VERSION = vk.API_VERSION_1_4; -// TODO: update before release -const DEBUG = true; +const DEBUG = @import("builtin").mode == .Debug; const INSTANCE_EXTENSIONS = [_][*:0]const u8{ vk.extensions.khr_get_physical_device_properties_2.name, From 1d04e110f33de1b2cfc18f5c6519fbbfad16ecf8 Mon Sep 17 00:00:00 2001 From: Mitchell Date: Sat, 14 Feb 2026 23:08:19 -0600 Subject: [PATCH 3/4] chore: compile ffmpeg and link static libraries --- build.zig | 96 +++++++++++++++++++++++++++++++------------ build.zig.zon | 8 +--- build_app_image.sh | 2 +- flake.nix | 8 ++-- src/audio_encoder.zig | 2 +- 5 files changed, 79 insertions(+), 37 deletions(-) diff --git a/build.zig b/build.zig index f97a754..25d92d9 100644 --- a/build.zig +++ b/build.zig @@ -65,20 +65,59 @@ fn addSharedDependencies( const zigrc = b.dependency("zigrc", .{}); exe.root_module.addImport("zigrc", zigrc.module("zigrc")); - // ffmpeg - // Add ffmpeg headers here. They can be shared cross platform. Libs - // are added separately because they are platform specific. + // ffmpeg headers are shared across platforms; libs are platform-specific. const ffmpeg = b.dependency("ffmpeg", .{}); - const ffmpeg_path = ffmpeg.path("").getPath3(b, null).root_dir.path.?; exe.addIncludePath(ffmpeg.path("")); +} + +const FfmpegBuild = struct { + step: *std.Build.Step, + /// Directory will contain the static libraries. + lib_dir: std.Build.LazyPath, +}; + +// Build ffmpeg static libraries. +// NOTE: If any new ffmpeg functionality is added, then features will +// need to be enabled here. +fn build_ffmpeg(b: *std.Build) FfmpegBuild { + const ffmpeg = b.dependency("ffmpeg", .{}); + const build_ffmpeg_step = b.addSystemCommand(&.{ + "bash", + "-lc", + \\set -euo pipefail + \\./configure \ + \\ --prefix="$1" \ + \\ --disable-all \ + \\ --disable-debug \ + \\ --disable-autodetect \ + \\ --disable-doc \ + \\ --disable-network \ + \\ --disable-programs \ + \\ --disable-shared \ + \\ --enable-avutil \ + \\ --enable-avcodec \ + \\ --enable-avformat \ + \\ --enable-avdevice \ + \\ --enable-avfilter \ + \\ --enable-swresample \ + \\ --enable-swscale \ + \\ --enable-small \ + \\ --disable-runtime-cpudetect \ + \\ --enable-protocol=file \ + \\ --enable-muxer=mov,mp4,wav \ + \\ --enable-encoder=aac,pcm_f32le + \\make -j + \\make install + , + "ffmpeg-build", + }); + build_ffmpeg_step.setCwd(ffmpeg.path("")); + const ffmpeg_install_prefix = build_ffmpeg_step.addOutputDirectoryArg("ffmpeg-install"); + build_ffmpeg_step.expectExitCode(0); - // TODO: Make sure this only runs once. Currently during fresh install - // it runs 3 times - one for each type of build. - (try std.fs.openDirAbsolute(ffmpeg_path, .{})).access("libavutil/avconfig.h", .{}) catch { - std.debug.print("configuring ffmpeg... this may take a minute\n", .{}); - const ffmpeg_configure_step = b.addSystemCommand(&.{"./configure"}); - ffmpeg_configure_step.setCwd(ffmpeg.path("")); - exe.step.dependOn(&ffmpeg_configure_step.step); + return .{ + .step = &build_ffmpeg_step.step, + .lib_dir = ffmpeg_install_prefix.path(b, "lib"), }; } @@ -88,6 +127,7 @@ fn addLinuxDependencies( exe: *std.Build.Step.Compile, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, + ffmpeg_build: FfmpegBuild, ) !void { _ = allocator; const pipewire = b.dependency("pipewire", .{ @@ -112,16 +152,15 @@ fn addLinuxDependencies( // system has the libs installed. exe.root_module.linkSystemLibrary("vulkan", .{}); - // TODO: Statically link ffmpeg with the zig version. - const ffmpeg_linux = b.dependency("ffmpeg_linux", .{}); - exe.addLibraryPath(ffmpeg_linux.path("lib")); - - exe.root_module.linkSystemLibrary("avformat", .{}); - exe.root_module.linkSystemLibrary("avcodec", .{}); - exe.root_module.linkSystemLibrary("avdevice", .{}); - exe.root_module.linkSystemLibrary("avfilter", .{}); - exe.root_module.linkSystemLibrary("avutil", .{}); - exe.root_module.linkSystemLibrary("swresample", .{}); + exe.step.dependOn(ffmpeg_build.step); + exe.addLibraryPath(ffmpeg_build.lib_dir); + exe.root_module.linkSystemLibrary("avformat", .{ .preferred_link_mode = .static }); + exe.root_module.linkSystemLibrary("avcodec", .{ .preferred_link_mode = .static }); + exe.root_module.linkSystemLibrary("avutil", .{ .preferred_link_mode = .static }); + exe.root_module.linkSystemLibrary("swresample", .{ .preferred_link_mode = .static }); + exe.root_module.linkSystemLibrary("avdevice", .{ .preferred_link_mode = .static }); + exe.root_module.linkSystemLibrary("avfilter", .{ .preferred_link_mode = .static }); + exe.root_module.linkSystemLibrary("swscale", .{ .preferred_link_mode = .static }); } /// Install a dynamic library in the /lib directory @@ -268,6 +307,7 @@ fn buildLinux( target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, nix: bool, + ffmpeg_build: FfmpegBuild, ) !*std.Build.Step { const module = b.createModule(.{ .root_source_file = b.path("src/main.zig"), @@ -293,7 +333,7 @@ fn buildLinux( } try addSharedDependencies(allocator, b, exe, target, optimize); - try addLinuxDependencies(allocator, b, exe, target, optimize); + try addLinuxDependencies(allocator, b, exe, target, optimize, ffmpeg_build); const install_step = b.addInstallArtifact(exe, .{ .dest_dir = .{ .override = .{ .custom = "linux" } }, @@ -343,6 +383,7 @@ fn buildUnitTestsDefault( b: *std.Build, target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, + ffmpeg_build: FfmpegBuild, ) !void { const unit_test_files = [_][]const u8{ "./src/test.zig", @@ -366,7 +407,7 @@ fn buildUnitTestsDefault( exe.linkLibC(); try addSharedDependencies(allocator, b, exe, target, optimize); - try addLinuxDependencies(allocator, b, exe, target, optimize); + try addLinuxDependencies(allocator, b, exe, target, optimize, ffmpeg_build); const run_exe_unit_tests = b.addRunArtifact(exe); @@ -399,7 +440,9 @@ pub fn build(b: *std.Build) !void { try buildWindows(allocator, b, optimize); - const target = if (nix == true) b.standardTargetOptions(.{}) else b.resolveTargetQuery(.{ + const ffmpeg_build = build_ffmpeg(b); + + const linux_target = if (nix == true) b.standardTargetOptions(.{}) else b.resolveTargetQuery(.{ .os_tag = .linux, .abi = .gnu, .cpu_arch = .x86_64, @@ -408,9 +451,10 @@ pub fn build(b: *std.Build) !void { const linux_install_step = try buildLinux( allocator, b, - target, + linux_target, optimize, nix, + ffmpeg_build, ); const appimage_step = buildLinuxAppImage(b, allocator, linux_install_step); @@ -418,5 +462,5 @@ pub fn build(b: *std.Build) !void { b.getInstallStep().dependOn(appimage_step); } - try buildUnitTestsDefault(allocator, b, target, optimize); + try buildUnitTestsDefault(allocator, b, linux_target, optimize, ffmpeg_build); } diff --git a/build.zig.zon b/build.zig.zon index 3611d4d..9ad98a5 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -83,12 +83,8 @@ .hash = "imguiz-0.0.0-sI63zx-_DwQbtJX5KbwrOK_VnEkxIft96REu9HVBZ3W8", }, .ffmpeg = .{ - .url = "https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n7.1.tar.gz", - .hash = "N-V-__8AAL-l8gSpW3fjjSbxZmbJt3OvF_ofxYzT8TsfzD9T", - }, - .ffmpeg_linux = .{ - .url = "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-02-10-13-08/ffmpeg-n7.1.3-40-gcddd06f3b9-linux64-gpl-shared-7.1.tar.xz", - .hash = "N-V-__8AAMWBGwzjWy3ngwsCARDuxQb9JkPi8B5Qwjhi8Fvk", + .url = "https://github.com/FFmpeg/FFmpeg/archive/refs/tags/n8.0.1.tar.gz", + .hash = "N-V-__8AAH4RHQXHEafp_hkUel3EMeK1wjHBfaIYYxYsKdiM", }, .ffmpeg_windows = .{ .url = "https://github.com/BtbN/FFmpeg-Builds/releases/download/autobuild-2026-02-10-13-08/ffmpeg-n7.1.3-40-gcddd06f3b9-win64-gpl-shared-7.1.zip", diff --git a/build_app_image.sh b/build_app_image.sh index 26324a0..3a750f0 100755 --- a/build_app_image.sh +++ b/build_app_image.sh @@ -9,7 +9,7 @@ rm -rf AppDir rm -f zig-out/linux/spacecap-linux-x86_64.AppImage # NOTE: Vulkan is excluded because system libraries should be used. -LD_LIBRARY_PATH="zig-out/linux/lib:${LD_LIBRARY_PATH:-}" linuxdeploy \ +LD_LIBRARY_PATH="${LD_LIBRARY_PATH:-}" linuxdeploy \ --appdir AppDir \ --executable zig-out/linux/spacecap \ --desktop-file packaging/linux/spacecap.desktop \ diff --git a/flake.nix b/flake.nix index b94e6ef..5a2e607 100644 --- a/flake.nix +++ b/flake.nix @@ -110,7 +110,6 @@ # For configuring ffmpeg headers nasm pkg-config - ffmpeg # Windows pkgsCross.mingwW64.vulkan-loader @@ -121,13 +120,16 @@ GLIB = "${pkgs.glib.out}/lib"; LIBPORTAL = "${pkgs.libportal}/lib"; + # Required for Github actions or non-NixOS machines. LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [ - # Required for Github actions. The runners don't provide fuse. pkgs.vulkan-loader pkgs.glib pkgs.libportal - pkgs.ffmpeg + # SDL runtime backends (don't rely on ffmpeg closure for these). + pkgs.wayland + pkgs.libxkbcommon ]; + # TODO: Separate devShell for building appimage. }; } diff --git a/src/audio_encoder.zig b/src/audio_encoder.zig index 490c87e..59f40b6 100644 --- a/src/audio_encoder.zig +++ b/src/audio_encoder.zig @@ -68,7 +68,7 @@ pub const AudioEncoder = struct { if (chosen_fmt == ffmpeg.AV_SAMPLE_FMT_NONE) return error.UnsupportedAudioSampleFormat; audio_codec_ctx.*.sample_fmt = chosen_fmt; - audio_codec_ctx.*.profile = ffmpeg.FF_PROFILE_AAC_LOW; + audio_codec_ctx.*.profile = ffmpeg.AV_PROFILE_AAC_LOW; if (format_context.oformat.*.flags & ffmpeg.AVFMT_GLOBALHEADER != 0) { audio_codec_ctx.*.flags |= ffmpeg.AV_CODEC_FLAG_GLOBAL_HEADER; From 05e33ec010497b5935e12185455b84c464c3b057 Mon Sep 17 00:00:00 2001 From: Mitchell Date: Sun, 15 Feb 2026 23:18:47 -0600 Subject: [PATCH 4/4] ci: fix issue with duplicated release notes --- .../workflows/{ci.yml => build_and_test.yml} | 0 .github/workflows/release.yml | 24 +++++++++++++++++-- build.zig.zon | 2 +- flake.nix | 2 -- 4 files changed, 23 insertions(+), 5 deletions(-) rename .github/workflows/{ci.yml => build_and_test.yml} (100%) diff --git a/.github/workflows/ci.yml b/.github/workflows/build_and_test.yml similarity index 100% rename from .github/workflows/ci.yml rename to .github/workflows/build_and_test.yml diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index e624e0a..ab9da32 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -4,7 +4,6 @@ on: push: branches: - main - - dev-14 tags: - "v*" @@ -32,12 +31,14 @@ jobs: - name: Resolve and Validate Release Version id: version run: | + # Grab the version from build.zig.zon. base_version="$(sed -n 's/^[[:space:]]*\.version = "\(.*\)",/\1/p' build.zig.zon | head -n1)" if [ -z "$base_version" ]; then echo "Failed to parse version from build.zig.zon" >&2 exit 1 fi + # If triggered by a new tag. if [ "${GITHUB_REF_TYPE}" = "tag" ]; then expected_tag="v${base_version}" actual_tag="${GITHUB_REF_NAME}" @@ -50,6 +51,7 @@ jobs: release_tag="${actual_tag}" release_version="${base_version}" prerelease="false" + # Triggered by merge to main. else short_sha="$(git rev-parse --short=8 HEAD)" release_version="${base_version}+g${short_sha}" @@ -101,13 +103,31 @@ jobs: git tag -f nightly "${GITHUB_SHA}" git push --force origin refs/tags/nightly + - name: Check Existing Release + id: existing_release + env: + GH_TOKEN: ${{ github.token }} + run: | + if gh release view "${{ steps.version.outputs.release_tag }}" > /dev/null 2>&1; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Delete Existing Nightly Release + if: github.ref_type != 'tag' && steps.existing_release.outputs.exists == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: gh release delete "${{ steps.version.outputs.release_tag }}" --yes + - name: Publish GitHub Release Assets uses: softprops/action-gh-release@v2 with: tag_name: ${{ steps.version.outputs.release_tag }} target_commitish: ${{ github.sha }} prerelease: ${{ steps.version.outputs.prerelease }} - generate_release_notes: true + generate_release_notes: ${{ github.ref_type != 'tag' || steps.existing_release.outputs.exists != 'true' }} + append_body: false # spacecap-linux-x86_64.tar.gz is currently disabled. # spacecap-windows-x86_64.zip is currently disabled. files: | diff --git a/build.zig.zon b/build.zig.zon index 9ad98a5..28d7bd6 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -10,7 +10,7 @@ // This is a [Semantic Version](https://semver.org/). // In a future version of Zig it will be used for package deduplication. - .version = "0.1.1", + .version = "0.1.0", // Together with name, this represents a globally unique package // identifier. This field is generated by the Zig toolchain when the diff --git a/flake.nix b/flake.nix index 5a2e607..e76098a 100644 --- a/flake.nix +++ b/flake.nix @@ -117,8 +117,6 @@ VK_LAYER_PATH = "${pkgs.vulkan-validation-layers}/share/vulkan/explicit_layer.d"; VULKAN_SDK_PATH_WINDOWS = "${pkgs.pkgsCross.mingwW64.vulkan-loader}/bin"; - GLIB = "${pkgs.glib.out}/lib"; - LIBPORTAL = "${pkgs.libportal}/lib"; # Required for Github actions or non-NixOS machines. LD_LIBRARY_PATH = pkgs.lib.makeLibraryPath [