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.
This commit is contained in:
Mitchell Hashimoto
2025-03-19 10:12:45 -07:00
parent bd315c8394
commit 7b8c2232d3
6 changed files with 236 additions and 92 deletions

View File

@ -239,7 +239,17 @@ jobs:
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}" authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: Build and Check Source Tarball - 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: build-macos:
runs-on: namespace-profile-ghostty-macos runs-on: namespace-profile-ghostty-macos
@ -828,7 +838,7 @@ jobs:
test-debian-12: test-debian-12:
name: Test build on Debian 12 name: Test build on Debian 12
runs-on: namespace-profile-ghostty-sm runs-on: namespace-profile-ghostty-sm
needs: test needs: [test, build-dist]
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -839,6 +849,11 @@ jobs:
- name: Configure Namespace powered Buildx - name: Configure Namespace powered Buildx
uses: namespacelabs/nscloud-setup-buildx-action@v0 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 - name: Build and push
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:

View File

@ -188,8 +188,11 @@ SENTRY_DSN=https://e914ee84fd895c4fe324afa3e53dac76@o4507352570920960.ingest.us.
## Developing Ghostty ## Developing Ghostty
See the documentation on the Ghostty website for See the documentation on the Ghostty website for
[building Ghostty from source](http://ghostty.org/docs/install/build). [building Ghostty from a source tarball](http://ghostty.org/docs/install/build).
For development, omit the `-Doptimize` flag to build a debug 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 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 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 in the current running terminal emulator so if you want to check the
behavior of this project, you must run this command in Ghostty. 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 ### Linting
#### Prettier #### Prettier

View File

@ -108,6 +108,12 @@ in
# Localization # Localization
gettext 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 [ ++ lib.optionals stdenv.hostPlatform.isLinux [
# My nix shell environment installs the non-interactive version # My nix shell environment installs the non-interactive version
@ -146,9 +152,6 @@ in
libXrandr libXrandr
# Only needed for GTK builds # Only needed for GTK builds
blueprint-compiler
libadwaita
gtk4
gtk4-layer-shell gtk4-layer-shell
glib glib
gobject-introspection gobject-introspection

View File

@ -17,6 +17,15 @@ archive_step: *std.Build.Step,
check_step: *std.Build.Step, check_step: *std.Build.Step,
pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist { 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 // git archive to create the final tarball. "git archive" is the
// easiest way I can find to create a tarball that ignores stuff // easiest way I can find to create a tarball that ignores stuff
// from gitignore and also supports adding files as well as removing // 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", "git",
"archive", "archive",
"--format=tgz", "--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 // This is important. Standard source tarballs extract into
// a directory named `project-version`. This is expected by // a directory named `project-version`. This is expected by
// standard tooling such as debhelper and rpmbuild. // standard tooling such as debhelper and rpmbuild.
b.fmt("--prefix=ghostty-{}/", .{cfg.version}), b.fmt("--prefix=ghostty-{}/", .{cfg.version}),
"-o", "-o",
}); });
const output = git_archive.addOutputFileArg(b.fmt( const output = git_archive.addOutputFileArg(b.fmt(
@ -78,6 +109,13 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist {
break :step step; 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 .{ return .{
.archive = output, .archive = output,
.install_step = &install.step, .install_step = &install.step,
@ -85,3 +123,51 @@ pub fn init(b: *std.Build, cfg: *const Config) !GhosttyDist {
.check_step = &check_test.step, .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;
}
}
};

View File

@ -6,6 +6,9 @@ const HelpStrings = @import("HelpStrings.zig");
const MetallibStep = @import("MetallibStep.zig"); const MetallibStep = @import("MetallibStep.zig");
const UnicodeTables = @import("UnicodeTables.zig"); const UnicodeTables = @import("UnicodeTables.zig");
const GhosttyFrameData = @import("GhosttyFrameData.zig"); const GhosttyFrameData = @import("GhosttyFrameData.zig");
const DistResource = @import("GhosttyDist.zig").Resource;
const gresource = @import("../apprt/gtk/gresource.zig");
config: *const Config, config: *const Config,
@ -659,54 +662,7 @@ fn addGTK(
} }
{ {
const gresource = @import("../apprt/gtk/gresource.zig"); // For our actual build, we validate our GTK builder files if we can.
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();
};
{ {
const gtk_builder_check = b.addExecutable(.{ const gtk_builder_check = b.addExecutable(.{
.name = "gtk_builder_check", .name = "gtk_builder_check",
@ -734,30 +690,98 @@ fn addGTK(
} }
} }
const generate_resources_c = b.addSystemCommand(&.{ // Get our gresource c/h files and add them to our build.
"glib-compile-resources", const dist = gtkDistResources(b);
"--c-name", step.addCSourceFile(.{ .file = dist.resources_c.path(b), .flags = &.{} });
"ghostty", step.addIncludePath(dist.resources_h.path(b).dirname());
"--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());
} }
} }
/// 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 // For dynamic linking, we prefer dynamic linking and to search by
// mode first. Mode first will search all paths for a dynamic library // mode first. Mode first will search all paths for a dynamic library
// before falling back to static. // before falling back to static.

View File

@ -5,7 +5,6 @@ FROM docker.io/library/debian:${DISTRO_VERSION}
RUN DEBIAN_FRONTEND="noninteractive" apt-get -qq update && \ RUN DEBIAN_FRONTEND="noninteractive" apt-get -qq update && \
apt-get -qq -y --no-install-recommends install \ apt-get -qq -y --no-install-recommends install \
# Build Tools # Build Tools
blueprint-compiler \
build-essential \ build-essential \
curl \ curl \
libbz2-dev \ libbz2-dev \
@ -37,25 +36,24 @@ RUN export ZIG_VERSION=$(sed -n -e 's/^.*requireZig("\(.*\)").*$/\1/p' /src/buil
rm /tmp/zig.tar.xz && \ rm /tmp/zig.tar.xz && \
ln -s "/opt/zig-linux-$(uname -m)-$ZIG_VERSION/zig" /usr/local/bin/zig 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 WORKDIR /src
RUN tar xvzf ghostty-source.tar.gz && \
COPY ./dist/linux /src/dist/linux rm ghostty-source.tar.gz && \
COPY ./images /src/images mv ghostty-* ghostty-source && \
COPY ./include /src/include mv ghostty-source/* . && \
COPY ./pkg /src/pkg rm -rf ghostty-source
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 ZIG_GLOBAL_CACHE_DIR=/zig/global-cache ./nix/build-support/fetch-zig-cache.sh 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 # 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 RUN ./zig-out/bin/ghostty +version