From 7b8c2232d3ea5d3693e40ce2aab91edd366951f6 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Wed, 19 Mar 2025 10:12:45 -0700 Subject: [PATCH] build: distribute gresource c/h with source tarball This introduces the concept of a "dist resource" (specifically a `GhosttyDist.Resource` type). This is a resource that may be present in dist tarballs but not in the source tree. If the resource is present and we're not in a Git checkout, then we use it directly instead of generating it. This is used for the first time in this commit for the gresource c/h files, which depend on a variety of external tools (blueprint-compiler, glib-compile-resources, etc.) that we do not want to require downstream users/packagers to have and we also do not want to worry about them having the right versions. This also adds a check for `distcheck` to ensure our distribution contains all the expected files. --- .github/workflows/test.yml | 19 +++- README.md | 22 +++- nix/devShell.nix | 9 +- src/build/GhosttyDist.zig | 88 +++++++++++++++- src/build/SharedDeps.zig | 162 +++++++++++++++++------------ src/build/docker/debian/Dockerfile | 28 +++-- 6 files changed, 236 insertions(+), 92 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cf0594709..aabd651b8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -239,7 +239,17 @@ jobs: authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" - name: Build and Check Source Tarball - run: nix develop -c zig build distcheck + run: | + rm -rf zig-out/dist + nix develop -c zig build distcheck + cp zig-out/dist/*.tar.gz ghostty-source.tar.gz + + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: source-tarball + path: |- + ghostty-source.tar.gz build-macos: runs-on: namespace-profile-ghostty-macos @@ -828,7 +838,7 @@ jobs: test-debian-12: name: Test build on Debian 12 runs-on: namespace-profile-ghostty-sm - needs: test + needs: [test, build-dist] steps: - name: Checkout code uses: actions/checkout@v4 @@ -839,6 +849,11 @@ jobs: - name: Configure Namespace powered Buildx uses: namespacelabs/nscloud-setup-buildx-action@v0 + - name: Download Source Tarball Artifacts + uses: actions/download-artifact@v4 + with: + name: source-tarball + - name: Build and push uses: docker/build-push-action@v6 with: diff --git a/README.md b/README.md index b2df95b13..d5c9dba02 100644 --- a/README.md +++ b/README.md @@ -188,8 +188,11 @@ SENTRY_DSN=https://e914ee84fd895c4fe324afa3e53dac76@o4507352570920960.ingest.us. ## Developing Ghostty See the documentation on the Ghostty website for -[building Ghostty from source](http://ghostty.org/docs/install/build). -For development, omit the `-Doptimize` flag to build a debug build. +[building Ghostty from a source tarball](http://ghostty.org/docs/install/build). +Building Ghostty from a Git checkout is very similar, except you want to +omit the `-Doptimize` flag to build a debug build, and you may require +additional dependencies since the source tarball includes some processed +files that are not in the Git repository. On Linux or macOS, you can use `zig build -Dapp-runtime=glfw run` for a quick GLFW-based app for a faster development cycle while developing core @@ -206,6 +209,21 @@ Other useful commands: in the current running terminal emulator so if you want to check the behavior of this project, you must run this command in Ghostty. +### Extra Dependencies + +Building Ghostty from a Git checkout on Linux requires some additional +dependencies: + +- `blueprint-compiler` + +macOS users don't require any additional dependencies. + +> [!NOTE] +> This only applies to building from a _Git checkout_. This section does +> not apply if you're building from a released _source tarball_. For +> source tarballs, see the +> [website](http://ghostty.org/docs/install/build). + ### Linting #### Prettier diff --git a/nix/devShell.nix b/nix/devShell.nix index 48b884bca..6949744d0 100644 --- a/nix/devShell.nix +++ b/nix/devShell.nix @@ -108,6 +108,12 @@ in # Localization gettext + + # We need these GTK-related deps on all platform so we can build + # dist tarballs. + blueprint-compiler + libadwaita + gtk4 ] ++ lib.optionals stdenv.hostPlatform.isLinux [ # My nix shell environment installs the non-interactive version @@ -146,9 +152,6 @@ in libXrandr # Only needed for GTK builds - blueprint-compiler - libadwaita - gtk4 gtk4-layer-shell glib gobject-introspection diff --git a/src/build/GhosttyDist.zig b/src/build/GhosttyDist.zig index 3218482a3..5af8b7480 100644 --- a/src/build/GhosttyDist.zig +++ b/src/build/GhosttyDist.zig @@ -17,6 +17,15 @@ archive_step: *std.Build.Step, check_step: *std.Build.Step, pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { + // Get the resources we're going to inject into the source tarball. + const alloc = b.allocator; + var resources: std.ArrayListUnmanaged(Resource) = .empty; + { + const gtk = SharedDeps.gtkDistResources(b); + try resources.append(alloc, gtk.resources_c); + try resources.append(alloc, gtk.resources_h); + } + // git archive to create the final tarball. "git archive" is the // easiest way I can find to create a tarball that ignores stuff // from gitignore and also supports adding files as well as removing @@ -25,12 +34,34 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { "git", "archive", "--format=tgz", + }); + // Add all of our resources into the tarball. + for (resources.items) |resource| { + // Our dist path basename may not match our generated file basename, + // and git archive requires this. To be safe, we copy the file once + // to ensure the basename matches and then use that as the final + // generated file. + const copied = b.addWriteFiles().addCopyFile( + resource.generated, + std.fs.path.basename(resource.dist), + ); + + // --add-file uses the most recent --prefix to determine the path + // in the archive to copy the file (the directory only). + git_archive.addArg(b.fmt("--prefix=ghostty-{}/{s}/", .{ + cfg.version, + std.fs.path.dirname(resource.dist).?, + })); + git_archive.addPrefixedFileArg("--add-file=", copied); + } + + // Add our output + git_archive.addArgs(&.{ // This is important. Standard source tarballs extract into // a directory named `project-version`. This is expected by // standard tooling such as debhelper and rpmbuild. b.fmt("--prefix=ghostty-{}/", .{cfg.version}), - "-o", }); const output = git_archive.addOutputFileArg(b.fmt( @@ -78,6 +109,13 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { break :step step; }; + // Check that all our dist resources are at the proper path. + for (resources.items) |resource| { + const path = extract_dir.path(b, resource.dist); + const check_path = b.addCheckFile(path, .{}); + check_test.step.dependOn(&check_path.step); + } + return .{ .archive = output, .install_step = &install.step, @@ -85,3 +123,51 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { .check_step = &check_test.step, }; } + +/// A dist resource is a resource that is built and distributed as part +/// of the source tarball with Ghostty. These aren't committed to the Git +/// repository but are built as part of the `zig build dist` command. +/// The purpose is to limit the number of build-time dependencies required +/// for downstream users and packagers. +pub const Resource = struct { + /// The relative path in the source tree where the resource will be + /// if it was pre-built. These are not checksummed or anything because the + /// assumption is that the source tarball itself is checksummed and signed. + dist: []const u8, + + /// The path to the generated resource in the build system. By depending + /// on this you'll force it to regenerate. This does NOT point to the + /// "path" above. + generated: std.Build.LazyPath, + + /// Returns the path to use for this resource. + pub fn path(self: *const Resource, b: *std.Build) std.Build.LazyPath { + // If the dist path exists at build compile time then we use it. + if (self.exists(b)) { + return b.path(self.dist); + } + + // Otherwise we use the generated path. + return self.generated; + } + + /// Returns true if the dist path exists at build time. + pub fn exists(self: *const Resource, b: *std.Build) bool { + if (std.fs.accessAbsolute(b.pathFromRoot(self.dist), .{})) { + // If we have a ".git" directory then we're a git checkout + // and we never want to use the dist path. This shouldn't happen + // so show a warning to the user. + if (std.fs.accessAbsolute(b.pathFromRoot(".git"), .{})) { + std.log.warn( + "dist resource '{s}' should not be in a git checkout", + .{self.dist}, + ); + return false; + } else |_| {} + + return true; + } else |_| { + return false; + } + } +}; diff --git a/src/build/SharedDeps.zig b/src/build/SharedDeps.zig index cf7602c2f..4f9373adb 100644 --- a/src/build/SharedDeps.zig +++ b/src/build/SharedDeps.zig @@ -6,6 +6,9 @@ const HelpStrings = @import("HelpStrings.zig"); const MetallibStep = @import("MetallibStep.zig"); const UnicodeTables = @import("UnicodeTables.zig"); const GhosttyFrameData = @import("GhosttyFrameData.zig"); +const DistResource = @import("GhosttyDist.zig").Resource; + +const gresource = @import("../apprt/gtk/gresource.zig"); config: *const Config, @@ -659,54 +662,7 @@ fn addGTK( } { - const gresource = @import("../apprt/gtk/gresource.zig"); - - const gresource_xml = gresource_xml: { - const generate_gresource_xml = b.addExecutable(.{ - .name = "generate_gresource_xml", - .root_source_file = b.path("src/apprt/gtk/gresource.zig"), - .target = b.graph.host, - }); - - const generate = b.addRunArtifact(generate_gresource_xml); - - const gtk_blueprint_compiler = b.addExecutable(.{ - .name = "gtk_blueprint_compiler", - .root_source_file = b.path("src/apprt/gtk/blueprint_compiler.zig"), - .target = b.graph.host, - }); - gtk_blueprint_compiler.linkSystemLibrary2("gtk4", dynamic_link_opts); - gtk_blueprint_compiler.linkSystemLibrary2("libadwaita-1", dynamic_link_opts); - gtk_blueprint_compiler.linkLibC(); - - for (gresource.blueprint_files) |blueprint_file| { - const blueprint_compiler = b.addRunArtifact(gtk_blueprint_compiler); - blueprint_compiler.addArgs(&.{ - b.fmt("{d}", .{blueprint_file.major}), - b.fmt("{d}", .{blueprint_file.minor}), - }); - const ui_file = blueprint_compiler.addOutputFileArg(b.fmt( - "{d}.{d}/{s}.ui", - .{ - blueprint_file.major, - blueprint_file.minor, - blueprint_file.name, - }, - )); - blueprint_compiler.addFileArg(b.path(b.fmt( - "src/apprt/gtk/ui/{d}.{d}/{s}.blp", - .{ - blueprint_file.major, - blueprint_file.minor, - blueprint_file.name, - }, - ))); - generate.addFileArg(ui_file); - } - - break :gresource_xml generate.captureStdOut(); - }; - + // For our actual build, we validate our GTK builder files if we can. { const gtk_builder_check = b.addExecutable(.{ .name = "gtk_builder_check", @@ -734,30 +690,98 @@ fn addGTK( } } - const generate_resources_c = b.addSystemCommand(&.{ - "glib-compile-resources", - "--c-name", - "ghostty", - "--generate-source", - "--target", - }); - const ghostty_resources_c = generate_resources_c.addOutputFileArg("ghostty_resources.c"); - generate_resources_c.addFileArg(gresource_xml); - step.addCSourceFile(.{ .file = ghostty_resources_c, .flags = &.{} }); - - const generate_resources_h = b.addSystemCommand(&.{ - "glib-compile-resources", - "--c-name", - "ghostty", - "--generate-header", - "--target", - }); - const ghostty_resources_h = generate_resources_h.addOutputFileArg("ghostty_resources.h"); - generate_resources_h.addFileArg(gresource_xml); - step.addIncludePath(ghostty_resources_h.dirname()); + // Get our gresource c/h files and add them to our build. + const dist = gtkDistResources(b); + step.addCSourceFile(.{ .file = dist.resources_c.path(b), .flags = &.{} }); + step.addIncludePath(dist.resources_h.path(b).dirname()); } } +/// Creates the resources that can be prebuilt for our dist build. +pub fn gtkDistResources( + b: *std.Build, +) struct { + resources_c: DistResource, + resources_h: DistResource, +} { + const gresource_xml = gresource_xml: { + const xml_exe = b.addExecutable(.{ + .name = "generate_gresource_xml", + .root_source_file = b.path("src/apprt/gtk/gresource.zig"), + .target = b.graph.host, + }); + const xml_run = b.addRunArtifact(xml_exe); + + const blueprint_exe = b.addExecutable(.{ + .name = "gtk_blueprint_compiler", + .root_source_file = b.path("src/apprt/gtk/blueprint_compiler.zig"), + .target = b.graph.host, + }); + blueprint_exe.linkLibC(); + blueprint_exe.linkSystemLibrary2("gtk4", dynamic_link_opts); + blueprint_exe.linkSystemLibrary2("libadwaita-1", dynamic_link_opts); + + for (gresource.blueprint_files) |blueprint_file| { + const blueprint_run = b.addRunArtifact(blueprint_exe); + blueprint_run.addArgs(&.{ + b.fmt("{d}", .{blueprint_file.major}), + b.fmt("{d}", .{blueprint_file.minor}), + }); + const ui_file = blueprint_run.addOutputFileArg(b.fmt( + "{d}.{d}/{s}.ui", + .{ + blueprint_file.major, + blueprint_file.minor, + blueprint_file.name, + }, + )); + blueprint_run.addFileArg(b.path(b.fmt( + "src/apprt/gtk/ui/{d}.{d}/{s}.blp", + .{ + blueprint_file.major, + blueprint_file.minor, + blueprint_file.name, + }, + ))); + + xml_run.addFileArg(ui_file); + } + + break :gresource_xml xml_run.captureStdOut(); + }; + + const generate_c = b.addSystemCommand(&.{ + "glib-compile-resources", + "--c-name", + "ghostty", + "--generate-source", + "--target", + }); + const resources_c = generate_c.addOutputFileArg("ghostty_resources.c"); + generate_c.addFileArg(gresource_xml); + + const generate_h = b.addSystemCommand(&.{ + "glib-compile-resources", + "--c-name", + "ghostty", + "--generate-header", + "--target", + }); + const resources_h = generate_h.addOutputFileArg("ghostty_resources.h"); + generate_h.addFileArg(gresource_xml); + + return .{ + .resources_c = .{ + .dist = "src/apprt/gtk/ghostty_resources.c", + .generated = resources_c, + }, + .resources_h = .{ + .dist = "src/apprt/gtk/ghostty_resources.h", + .generated = resources_h, + }, + }; +} + // For dynamic linking, we prefer dynamic linking and to search by // mode first. Mode first will search all paths for a dynamic library // before falling back to static. diff --git a/src/build/docker/debian/Dockerfile b/src/build/docker/debian/Dockerfile index 99a20c818..a29963f75 100644 --- a/src/build/docker/debian/Dockerfile +++ b/src/build/docker/debian/Dockerfile @@ -5,7 +5,6 @@ FROM docker.io/library/debian:${DISTRO_VERSION} RUN DEBIAN_FRONTEND="noninteractive" apt-get -qq update && \ apt-get -qq -y --no-install-recommends install \ # Build Tools - blueprint-compiler \ build-essential \ curl \ libbz2-dev \ @@ -37,25 +36,24 @@ RUN export ZIG_VERSION=$(sed -n -e 's/^.*requireZig("\(.*\)").*$/\1/p' /src/buil rm /tmp/zig.tar.xz && \ ln -s "/opt/zig-linux-$(uname -m)-$ZIG_VERSION/zig" /usr/local/bin/zig +# Extract our source tarball +COPY ./ghostty-source.tar.gz /src WORKDIR /src - -COPY ./dist/linux /src/dist/linux -COPY ./images /src/images -COPY ./include /src/include -COPY ./pkg /src/pkg -COPY ./po /src/po -COPY ./nix /src/nix -COPY ./vendor /src/vendor -COPY ./build.zig /src/build.zig -COPY ./build.zig.zon /src/build.zig.zon -COPY ./build.zig.zon.txt /src/build.zig.zon.txt +RUN tar xvzf ghostty-source.tar.gz && \ + rm ghostty-source.tar.gz && \ + mv ghostty-* ghostty-source && \ + mv ghostty-source/* . && \ + rm -rf ghostty-source RUN ZIG_GLOBAL_CACHE_DIR=/zig/global-cache ./nix/build-support/fetch-zig-cache.sh -COPY ./src /src/src - # Debian 12 doesn't have gtk4-layer-shell, so we have to manually compile it ourselves -RUN zig build -Doptimize=Debug -Dcpu=baseline -Dapp-runtime=gtk -fno-sys=gtk4-layer-shell --system /zig/global-cache/p +RUN zig build \ + -Doptimize=Debug \ + -Dcpu=baseline \ + -Dapp-runtime=gtk \ + -fno-sys=gtk4-layer-shell \ + --system /zig/global-cache/p RUN ./zig-out/bin/ghostty +version