Merge pull request #1653 from mitchellh/coretext

Enable CoreText font shaper for macOS by default
This commit is contained in:
Mitchell Hashimoto
2024-04-04 21:44:14 -07:00
committed by GitHub
12 changed files with 826 additions and 220 deletions

View File

@ -149,3 +149,146 @@ jobs:
r2-bucket: ghostty-pr
source-dir: blob
destination-dir: ./
build-macos-debug:
runs-on: namespace-profile-ghostty-macos
timeout-minutes: 90
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
# Important so that build number generation works
fetch-depth: 0
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@v26
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v14
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
# Setup Sparkle
- name: Setup Sparkle
env:
SPARKLE_VERSION: 2.5.1
run: |
mkdir -p .action/sparkle
cd .action/sparkle
curl -L https://github.com/sparkle-project/Sparkle/releases/download/${SPARKLE_VERSION}/Sparkle-for-Swift-Package-Manager.zip > sparkle.zip
unzip sparkle.zip
echo "$(pwd)/bin" >> $GITHUB_PATH
# Load Build Number
- name: Build Number
run: |
echo "GHOSTTY_BUILD=$(git rev-list --count head)" >> $GITHUB_ENV
echo "GHOSTTY_COMMIT=$(git rev-parse --short HEAD)" >> $GITHUB_ENV
# GhosttyKit is the framework that is built from Zig for our native
# Mac app to access. Build this in release mode.
- name: Build GhosttyKit
run: nix develop -c zig build -Dstatic=true -Doptimize=Debug
# The native app is built with native XCode tooling. This also does
# codesigning. IMPORTANT: this must NOT run in a Nix environment.
# Nix breaks xcodebuild so this has to be run outside.
- name: Build Ghostty.app
run: cd macos && xcodebuild -target Ghostty -configuration Release
# We inject the "build number" as simply the number of commits since HEAD.
# This will be a monotonically always increasing build number that we use.
- name: Update Info.plist
env:
SPARKLE_KEY_PUB: ${{ secrets.PROD_MACOS_SPARKLE_KEY_PUB }}
run: |
# Version Info
/usr/libexec/PlistBuddy -c "Set :GhosttyCommit $GHOSTTY_COMMIT" "macos/build/Release/Ghostty.app/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion $GHOSTTY_BUILD" "macos/build/Release/Ghostty.app/Contents/Info.plist"
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $GHOSTTY_COMMIT" "macos/build/Release/Ghostty.app/Contents/Info.plist"
# Updater
/usr/libexec/PlistBuddy -c "Set :SUPublicEDKey $SPARKLE_KEY_PUB" "macos/build/Release/Ghostty.app/Contents/Info.plist"
- name: Codesign app bundle
env:
MACOS_CERTIFICATE: ${{ secrets.PROD_MACOS_CERTIFICATE }}
MACOS_CERTIFICATE_PWD: ${{ secrets.PROD_MACOS_CERTIFICATE_PWD }}
MACOS_CERTIFICATE_NAME: ${{ secrets.PROD_MACOS_CERTIFICATE_NAME }}
MACOS_CI_KEYCHAIN_PWD: ${{ secrets.PROD_MACOS_CI_KEYCHAIN_PWD }}
run: |
# Turn our base64-encoded certificate back to a regular .p12 file
echo $MACOS_CERTIFICATE | base64 --decode > certificate.p12
# We need to create a new keychain, otherwise using the certificate will prompt
# with a UI dialog asking for the certificate password, which we can't
# use in a headless CI environment
security create-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$MACOS_CI_KEYCHAIN_PWD" build.keychain
security import certificate.p12 -k build.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MACOS_CI_KEYCHAIN_PWD" build.keychain
# Codesign Sparkle. Some notes here:
# - The XPC services aren't used since we don't sandbox Ghostty,
# but since they're part of the build, they still need to be
# codesigned.
# - The binaries in the "Versions" folders need to NOT be symlinks.
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Downloader.xpc"
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/XPCServices/Installer.xpc"
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/Autoupdate"
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework/Versions/B/Updater.app"
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime "macos/build/Release/Ghostty.app/Contents/Frameworks/Sparkle.framework"
# Codesign the app bundle
/usr/bin/codesign --verbose -f -s "$MACOS_CERTIFICATE_NAME" -o runtime --entitlements "macos/Ghostty.entitlements" macos/build/Release/Ghostty.app
- name: "Notarize app bundle"
env:
PROD_MACOS_NOTARIZATION_APPLE_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_APPLE_ID }}
PROD_MACOS_NOTARIZATION_TEAM_ID: ${{ secrets.PROD_MACOS_NOTARIZATION_TEAM_ID }}
PROD_MACOS_NOTARIZATION_PWD: ${{ secrets.PROD_MACOS_NOTARIZATION_PWD }}
run: |
# Store the notarization credentials so that we can prevent a UI password dialog
# from blocking the CI
echo "Create keychain profile"
xcrun notarytool store-credentials "notarytool-profile" --apple-id "$PROD_MACOS_NOTARIZATION_APPLE_ID" --team-id "$PROD_MACOS_NOTARIZATION_TEAM_ID" --password "$PROD_MACOS_NOTARIZATION_PWD"
# We can't notarize an app bundle directly, but we need to compress it as an archive.
# Therefore, we create a zip file containing our app bundle, so that we can send it to the
# notarization service
echo "Creating temp notarization archive"
ditto -c -k --keepParent "macos/build/Release/Ghostty.app" "notarization.zip"
# Here we send the notarization request to the Apple's Notarization service, waiting for the result.
# This typically takes a few seconds inside a CI environment, but it might take more depending on the App
# characteristics. Visit the Notarization docs for more information and strategies on how to optimize it if
# you're curious
echo "Notarize app"
xcrun notarytool submit "notarization.zip" --keychain-profile "notarytool-profile" --wait
# Finally, we need to "attach the staple" to our executable, which will allow our app to be
# validated by macOS even when an internet connection is not available.
echo "Attach staple"
xcrun stapler staple "macos/build/Release/Ghostty.app"
# Zip up the app
- name: Zip App
run: cd macos/build/Release && zip -9 -r --symlinks ../../../ghostty-macos-universal-debug.zip Ghostty.app
# Update Blob Storage
- name: Prep R2 Storage
run: |
mkdir blob
mkdir -p blob/${GHOSTTY_BUILD}
cp ghostty-macos-universal-debug.zip blob/${GHOSTTY_BUILD}/ghostty-macos-universal-debug.zip
- name: Upload to R2
uses: ryand56/r2-upload-action@latest
with:
r2-account-id: ${{ secrets.CF_R2_PR_ACCOUNT_ID }}
r2-access-key-id: ${{ secrets.CF_R2_PR_AWS_KEY }}
r2-secret-access-key: ${{ secrets.CF_R2_PR_SECRET_KEY }}
r2-bucket: ghostty-pr
source-dir: blob
destination-dir: ./

View File

@ -277,6 +277,25 @@ jobs:
- name: Test Dynamic Build
run: nix develop -c zig build -Dstatic=false
test-macos:
runs-on: namespace-profile-ghostty-macos
needs: test
steps:
- name: Checkout code
uses: actions/checkout@v4
# Install Nix and use that to run our tests so our environment matches exactly.
- uses: cachix/install-nix-action@v26
with:
nix_path: nixpkgs=channel:nixos-unstable
- uses: cachix/cachix-action@v14
with:
name: ghostty
authToken: "${{ secrets.CACHIX_AUTH_TOKEN }}"
- name: test
run: nix develop -c zig build test
prettier:
runs-on: namespace-profile-ghostty-sm
timeout-minutes: 60

View File

@ -1055,11 +1055,14 @@ fn addDeps(
"fontconfig",
fontconfig_dep.module("fontconfig"),
);
if (config.font_backend.hasHarfbuzz()) step.root_module.addImport(
"harfbuzz",
harfbuzz_dep.module("harfbuzz"),
);
step.root_module.addImport("oniguruma", oniguruma_dep.module("oniguruma"));
step.root_module.addImport("freetype", freetype_dep.module("freetype"));
step.root_module.addImport("glslang", glslang_dep.module("glslang"));
step.root_module.addImport("spirv_cross", spirv_cross_dep.module("spirv_cross"));
step.root_module.addImport("harfbuzz", harfbuzz_dep.module("harfbuzz"));
step.root_module.addImport("xev", libxev_dep.module("xev"));
step.root_module.addImport("opengl", opengl_dep.module("opengl"));
step.root_module.addImport("pixman", pixman_dep.module("pixman"));
@ -1110,7 +1113,6 @@ fn addDeps(
step.addIncludePath(freetype_dep.path(""));
step.linkSystemLibrary2("bzip2", dynamic_link_opts);
step.linkSystemLibrary2("freetype2", dynamic_link_opts);
step.linkSystemLibrary2("harfbuzz", dynamic_link_opts);
step.linkSystemLibrary2("libpng", dynamic_link_opts);
step.linkSystemLibrary2("oniguruma", dynamic_link_opts);
step.linkSystemLibrary2("pixman-1", dynamic_link_opts);
@ -1119,6 +1121,9 @@ fn addDeps(
if (config.font_backend.hasFontconfig()) {
step.linkSystemLibrary2("fontconfig", dynamic_link_opts);
}
if (config.font_backend.hasHarfbuzz()) {
step.linkSystemLibrary2("harfbuzz", dynamic_link_opts);
}
}
// Other dependencies, we may dynamically link
@ -1136,14 +1141,16 @@ fn addDeps(
step.linkLibrary(freetype_dep.artifact("freetype"));
try static_libs.append(freetype_dep.artifact("freetype").getEmittedBin());
// Harfbuzz
step.linkLibrary(harfbuzz_dep.artifact("harfbuzz"));
try static_libs.append(harfbuzz_dep.artifact("harfbuzz").getEmittedBin());
// Pixman
step.linkLibrary(pixman_dep.artifact("pixman"));
try static_libs.append(pixman_dep.artifact("pixman").getEmittedBin());
// Harfbuzz
if (config.font_backend.hasHarfbuzz()) {
step.linkLibrary(harfbuzz_dep.artifact("harfbuzz"));
try static_libs.append(harfbuzz_dep.artifact("harfbuzz").getEmittedBin());
}
// Only Linux gets fontconfig
if (config.font_backend.hasFontconfig()) {
// Fontconfig

View File

@ -1,6 +1,7 @@
const std = @import("std");
const Allocator = std.mem.Allocator;
const base = @import("base.zig");
const c = @import("c.zig");
const cftype = @import("type.zig");
const ComparisonResult = base.ComparisonResult;
const Range = base.Range;
@ -42,6 +43,14 @@ pub const Array = opaque {
};
pub const MutableArray = opaque {
pub fn create() Allocator.Error!*MutableArray {
return CFArrayCreateMutable(
null,
0,
&c.kCFTypeArrayCallBacks,
) orelse error.OutOfMemory;
}
pub fn createCopy(array: *Array) Allocator.Error!*MutableArray {
return CFArrayCreateMutableCopy(
null,
@ -54,6 +63,18 @@ pub const MutableArray = opaque {
cftype.CFRelease(self);
}
pub fn appendValue(
self: *MutableArray,
comptime Elem: type,
value: *const Elem,
) void {
CFArrayAppendValue(self, @constCast(@ptrCast(value)));
}
pub fn removeValue(self: *MutableArray, idx: usize) void {
CFArrayRemoveValueAtIndex(self, idx);
}
pub fn sortValues(
self: *MutableArray,
comptime Elem: type,
@ -73,12 +94,24 @@ pub const MutableArray = opaque {
);
}
extern "c" fn CFArrayCreateMutable(
allocator: ?*anyopaque,
capacity: usize,
callbacks: ?*const anyopaque,
) ?*MutableArray;
extern "c" fn CFArrayCreateMutableCopy(
allocator: ?*anyopaque,
capacity: usize,
array: *Array,
) ?*MutableArray;
extern "c" fn CFArrayAppendValue(
*MutableArray,
*anyopaque,
) void;
extern "c" fn CFArrayRemoveValueAtIndex(
*MutableArray,
usize,
) void;
extern "c" fn CFArraySortValues(
array: *MutableArray,
range: Range,

View File

@ -6,8 +6,8 @@ const c = @import("c.zig");
pub const Dictionary = opaque {
pub fn create(
keys: ?[]?*const anyopaque,
values: ?[]?*const anyopaque,
keys: ?[]const ?*const anyopaque,
values: ?[]const ?*const anyopaque,
) Allocator.Error!*Dictionary {
if (keys != null or values != null) {
assert(keys != null);
@ -17,8 +17,8 @@ pub const Dictionary = opaque {
return @as(?*Dictionary, @ptrFromInt(@intFromPtr(c.CFDictionaryCreate(
null,
@ptrCast(if (keys) |slice| slice.ptr else null),
@ptrCast(if (values) |slice| slice.ptr else null),
@constCast(@ptrCast(if (keys) |slice| slice.ptr else null)),
@constCast(@ptrCast(if (values) |slice| slice.ptr else null)),
@intCast(if (keys) |slice| slice.len else 0),
&c.kCFTypeDictionaryKeyCallBacks,
&c.kCFTypeDictionaryValueCallBacks,

View File

@ -13,8 +13,9 @@ pub const Face = struct {
/// Our font face
font: *macos.text.Font,
/// Harfbuzz font corresponding to this face.
hb_font: harfbuzz.Font,
/// Harfbuzz font corresponding to this face. We only use this
/// if we're using Harfbuzz.
hb_font: if (harfbuzz_shaper) harfbuzz.Font else void,
/// The presentation for this font.
presentation: font.Presentation,
@ -25,6 +26,10 @@ pub const Face = struct {
/// Set quirks.disableDefaultFontFeatures
quirks_disable_default_font_features: bool = false,
/// True if our build is using Harfbuzz. If we're not, we can avoid
/// some Harfbuzz-specific code paths.
const harfbuzz_shaper = font.options.backend.hasHarfbuzz();
/// The matrix applied to a regular font to auto-italicize it.
pub const italic_skew = macos.graphics.AffineTransform{
.a = 1,
@ -75,10 +80,6 @@ pub const Face = struct {
/// Initialize a face with a CTFont. This will take ownership over
/// the CTFont. This does NOT copy or retain the CTFont.
pub fn initFont(ct_font: *macos.text.Font, opts: font.face.Options) !Face {
var hb_font = try harfbuzz.coretext.createFont(ct_font);
errdefer hb_font.destroy();
hb_font.setScale(opts.size.pixels(), opts.size.pixels());
const traits = ct_font.getSymbolicTraits();
const metrics = metrics: {
var metrics = try calcMetrics(ct_font);
@ -86,6 +87,13 @@ pub const Face = struct {
break :metrics metrics;
};
var hb_font = if (comptime harfbuzz_shaper) font: {
var hb_font = try harfbuzz.coretext.createFont(ct_font);
hb_font.setScale(opts.size.pixels(), opts.size.pixels());
break :font hb_font;
} else {};
errdefer if (comptime harfbuzz_shaper) hb_font.destroy();
var result: Face = .{
.font = ct_font,
.hb_font = hb_font,
@ -144,7 +152,7 @@ pub const Face = struct {
pub fn deinit(self: *Face) void {
self.font.release();
self.hb_font.destroy();
if (comptime harfbuzz_shaper) self.hb_font.destroy();
self.* = undefined;
}

View File

@ -115,6 +115,19 @@ pub const Backend = enum {
=> false,
};
}
pub fn hasHarfbuzz(self: Backend) bool {
return switch (self) {
.freetype,
.fontconfig_freetype,
.coretext_freetype,
=> true,
.coretext,
.web_canvas,
=> false,
};
}
};
/// The styles that a family can take.

Binary file not shown.

View File

@ -1,7 +1,7 @@
const builtin = @import("builtin");
const options = @import("main.zig").options;
const harfbuzz = @import("shaper/harfbuzz.zig");
const coretext = @import("shaper/coretext.zig");
pub const harfbuzz = @import("shaper/harfbuzz.zig");
pub const coretext = @import("shaper/coretext.zig");
pub const web_canvas = @import("shaper/web_canvas.zig");
pub usingnamespace @import("shaper/run.zig");
@ -10,12 +10,12 @@ pub const Shaper = switch (options.backend) {
.freetype,
.fontconfig_freetype,
.coretext_freetype,
.coretext,
=> harfbuzz.Shaper,
// Has missing features, can't be used yet. See the comments in
// the coretext.zig file for more details.
//.coretext => coretext.Shaper,
// Note that coretext_freetype cannot use the coretext
// shaper because the coretext shaper requests CoreText
// font faces.
.coretext => coretext.Shaper,
.web_canvas => web_canvas.Shaper,
};

File diff suppressed because it is too large Load Diff

View File

@ -12,6 +12,9 @@ pub const fontEmoji = @embedFile("res/NotoColorEmoji.ttf");
pub const fontEmojiText = @embedFile("res/NotoEmoji-Regular.ttf");
pub const fontVariable = @embedFile("res/Lilex-VF.ttf");
/// Font with nerd fonts embedded.
pub const fontNerdFont = @embedFile("res/JetBrainsMonoNerdFont-Regular.ttf");
/// Cozette is a unique font because it embeds some emoji characters
/// but has a text presentation.
pub const fontCozette = @embedFile("res/CozetteVector.ttf");

View File

@ -256,7 +256,9 @@ pub const GlobalState = struct {
std.log.info("ghostty build optimize={s}", .{build_config.mode_string});
std.log.info("runtime={}", .{build_config.app_runtime});
std.log.info("font_backend={}", .{build_config.font_backend});
std.log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()});
if (comptime build_config.font_backend.hasHarfbuzz()) {
std.log.info("dependency harfbuzz={s}", .{harfbuzz.versionString()});
}
if (comptime build_config.font_backend.hasFontconfig()) {
std.log.info("dependency fontconfig={d}", .{fontconfig.version()});
}