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 }}"
- 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:

View File

@ -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

View File

@ -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

View File

@ -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;
}
}
};

View File

@ -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.

View File

@ -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