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

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