Compare commits

...

22 Commits

Author SHA1 Message Date
387c2c40aa docs: add CLAUDE.md for project guidance
- Introduced CLAUDE.md to provide comprehensive guidance on the macOS Emacs building system, including project overview, common commands, architecture details, and testing instructions.
2026-01-14 18:56:25 +00:00
527f855fbf chore(deps): add macOS SDK version 26
- Added support for macOS SDK version 26 to the list of supported SDKs.
- Reformatted flake.nix with nixfmt.
2026-01-14 18:53:56 +00:00
aafac57b1d chore(deps): update flake.lock and flake.pkgs for dependency versions
- Updated nixpkgs revision and narHash in flake.lock.
- Upgraded expat to version 2.7.3 and openssl to version 3.4.3 in flake.pkgs.
- Modified flake-package-versions command in Makefile for improved output.
2026-01-14 18:50:55 +00:00
jimehbot[bot]
4367d6b80e chore(master): release 0.6.63 (#149)
Co-authored-by: jimehbot[bot] <132453784+jimehbot[bot]@users.noreply.github.com>
2025-09-14 21:01:51 +01:00
b5ed3887c1 feat(icon): add support for Tahoe icons with --tahoe-icon-uri and --tahoe-icon-name options 2025-09-14 20:58:12 +01:00
607076a91b feat(icon): add support for custom application icons via --icon-uri option 2025-09-14 20:57:17 +01:00
1cda2cc3ff style(lint): resolve Naming/PredicateName rubocop complaint 2025-09-14 20:55:50 +01:00
jimehbot[bot]
39a4868313 chore(master): release 0.6.62 (#148)
Co-authored-by: jimehbot[bot] <132453784+jimehbot[bot]@users.noreply.github.com>
2025-09-14 12:25:30 +01:00
2086a773ae feat(deps/nix): upgrade build-time dependencies
In particular, this brings tree-sitter up to v0.25.3, enabling
tree-sitter ABI v15.
2025-09-14 12:23:27 +01:00
jimehbot[bot]
68ecce6158 chore(master): release 0.6.61 (#145)
Co-authored-by: jimehbot[bot] <132453784+jimehbot[bot]@users.noreply.github.com>
2025-08-13 01:40:23 +01:00
7aa4058128 fix(emacs-31-builds): disable fix-window-role patch after it was merged upstream 2025-08-13 01:38:46 +01:00
jimehbot[bot]
67ad73e500 chore(master): release 0.6.60 (#141)
Co-authored-by: jimehbot[bot] <132453784+jimehbot[bot]@users.noreply.github.com>
2025-06-27 22:42:03 +01:00
8ac1f946dd fix(builder/cask): correctly resolve version with build variant to release name (#140) 2025-06-27 22:40:57 +01:00
jimehbot[bot]
cc38319b40 chore(master): release 0.6.59 (#139)
Co-authored-by: jimehbot[bot] <132453784+jimehbot[bot]@users.noreply.github.com>
2025-06-27 15:25:59 +01:00
e8885400e6 fix(builder/plan): append test build name to release name (#138) 2025-06-27 15:24:55 +01:00
github-actions[bot]
00015d861f chore(master): release 0.6.58 (#135)
Co-authored-by: jimehbot[bot] <132453784+jimehbot[bot]@users.noreply.github.com>
2025-06-27 11:41:15 +01:00
ca8b874be2 feat(builder/plan): add build variant flag (#137) 2025-06-27 11:39:52 +01:00
5d36f02cca ci(release-please): use custom bot account to publish release PRs 2025-06-27 11:32:25 +01:00
5c37e8b0a2 ci(permissions): set correct permissions for release-please job 2025-06-27 11:27:56 +01:00
28ff28b29a chore(ci/deps): upgrade CI dependencies and fix linting issues (#136) 2025-06-27 11:25:16 +01:00
3cf1977def fix(deps): work around duplicate RPATHs in libgccjit from Nix (#134) 2025-06-27 11:05:46 +01:00
bc2a45767e chore(deps): use HTTPS for Gemfile source directive 2025-06-26 20:02:08 +01:00
20 changed files with 829 additions and 246 deletions

View File

@@ -1,3 +1,3 @@
{
".": "0.6.57"
".": "0.6.63"
}

View File

@@ -1,5 +1,6 @@
{
"bootstrap-sha": "3d6c7fff64bda8ba0dbea181c9f94fb9716dd188",
"always-update": true,
"packages": {
".": {
"release-type": "simple",

View File

@@ -1,6 +1,8 @@
---
name: CI
on: [push]
permissions:
contents: read
jobs:
lint:
@@ -10,11 +12,11 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.23"
go-version-file: go.mod
- name: golangci-lint
uses: golangci/golangci-lint-action@v6
uses: golangci/golangci-lint-action@v8
with:
version: v1.61
version: v2.1
env:
VERBOSE: "true"
@@ -25,7 +27,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.23"
go-version-file: go.mod
- name: Check if mods are tidy
run: make check-tidy
@@ -36,14 +38,21 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version: "1.23"
go-version-file: go.mod
- name: Run tests
run: make test
env:
VERBOSE: "true"
release-please:
name: Release Please
runs-on: ubuntu-latest
if: github.ref_name == 'main' || github.ref_name == 'master'
if: ${{ github.ref_name == 'main' || github.ref_name == 'master' }}
permissions:
contents: write
pull-requests: write
steps:
- uses: jimeh/release-please-manifest-action@v1
- uses: jimeh/release-please-manifest-action@v2
with:
app-id: ${{ secrets.RELEASE_BOT_APP_ID }}
private-key: ${{ secrets.RELEASE_BOT_PRIVATE_KEY }}

View File

@@ -1,25 +1,9 @@
linters-settings:
funlen:
lines: 100
statements: 150
goconst:
min-occurrences: 5
gocyclo:
min-complexity: 20
govet:
enable-all: true
disable:
- fieldalignment
lll:
line-length: 80
tab-width: 4
maligned:
suggest-new: true
misspell:
locale: US
version: "2"
run:
modules-download-mode: readonly
allow-parallel-runners: true
linters:
disable-all: true
default: none
enable:
- bodyclose
- copyloopvar
@@ -30,12 +14,8 @@ linters:
- goconst
- gocritic
- gocyclo
- gofumpt
- goimports
- goprintffuncname
- goprintffuncname
- gosec
- gosimple
- govet
- ineffassign
- lll
@@ -47,36 +27,69 @@ linters:
- revive
- sqlclosecheck
- staticcheck
- typecheck
- unconvert
- unused
- whitespace
issues:
exclude:
- Using the variable on range scope `tt` in function literal
- Using the variable on range scope `tc` in function literal
exclude-rules:
- path: "_test\\.go"
linters:
- funlen
- dupl
- goconst
- source: "^//go:generate "
linters:
- lll
- source: "`json:"
linters:
- lll
- source: "`yaml:"
linters:
- lll
exclude-dirs:
- builds
- sources
- tarballs
run:
timeout: 2m
allow-parallel-runners: true
modules-download-mode: readonly
settings:
funlen:
lines: 100
statements: 150
goconst:
min-occurrences: 5
gocyclo:
min-complexity: 20
govet:
disable:
- fieldalignment
enable-all: true
lll:
line-length: 80
tab-width: 4
misspell:
locale: US
exclusions:
generated: lax
presets:
- comments
- common-false-positives
- legacy
- std-error-handling
rules:
- linters:
- dupl
- funlen
- goconst
path: _test\.go
- linters:
- lll
source: "^//go:generate "
- linters:
- lll
source: "`json:"
- linters:
- lll
source: "`yaml:"
- path: (.+)\.go$
text: Using the variable on range scope `tt` in function literal
- path: (.+)\.go$
text: Using the variable on range scope `tc` in function literal
paths:
- builds
- sources
- tarballs
- third_party$
- builtin$
- examples$
formatters:
enable:
- gofumpt
- goimports
exclusions:
generated: lax
paths:
- builds
- sources
- tarballs
- third_party$
- builtin$
- examples$

View File

@@ -1,5 +1,53 @@
# Changelog
## [0.6.63](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.62...v0.6.63) (2025-09-14)
### Features
* **icon:** add support for custom application icons via --icon-uri option ([607076a](https://github.com/jimeh/build-emacs-for-macos/commit/607076a91bf0f227d16c9404f01a64144290685a))
* **icon:** add support for Tahoe icons with --tahoe-icon-uri and --tahoe-icon-name options ([b5ed388](https://github.com/jimeh/build-emacs-for-macos/commit/b5ed3887c172540de4a6190072b1e15f5d5efe2c))
## [0.6.62](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.61...v0.6.62) (2025-09-14)
### Features
* **deps/nix:** upgrade build-time dependencies ([2086a77](https://github.com/jimeh/build-emacs-for-macos/commit/2086a773ae6bf5f4c2e6863d1b06a85acc082e6d))
## [0.6.61](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.60...v0.6.61) (2025-08-13)
### Bug Fixes
* **emacs-31-builds:** disable fix-window-role patch after it was merged upstream ([7aa4058](https://github.com/jimeh/build-emacs-for-macos/commit/7aa405812802abbd2b78d8c98aee7fca23a2eab2))
## [0.6.60](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.59...v0.6.60) (2025-06-27)
### Bug Fixes
* **builder/cask:** correctly resolve version with build variant to release name ([#140](https://github.com/jimeh/build-emacs-for-macos/issues/140)) ([8ac1f94](https://github.com/jimeh/build-emacs-for-macos/commit/8ac1f946dde2342fa82aff7f90d2126bdd1f0057))
## [0.6.59](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.58...v0.6.59) (2025-06-27)
### Bug Fixes
* **builder/plan:** append test build name to release name ([#138](https://github.com/jimeh/build-emacs-for-macos/issues/138)) ([e888540](https://github.com/jimeh/build-emacs-for-macos/commit/e8885400e66bdb9304f99d9b072aa4dec4e83f4b))
## [0.6.58](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.57...v0.6.58) (2025-06-27)
### Features
* **builder/plan:** add build variant flag ([#137](https://github.com/jimeh/build-emacs-for-macos/issues/137)) ([ca8b874](https://github.com/jimeh/build-emacs-for-macos/commit/ca8b874be2c8c52cd7dcb05ff1348469e16c74ba))
### Bug Fixes
* **deps:** work around duplicate RPATHs in libgccjit from Nix ([#134](https://github.com/jimeh/build-emacs-for-macos/issues/134)) ([3cf1977](https://github.com/jimeh/build-emacs-for-macos/commit/3cf1977def02d1f3732b1051bc07a923557f9edd))
## [0.6.57](https://github.com/jimeh/build-emacs-for-macos/compare/v0.6.56...v0.6.57) (2024-12-07)

80
CLAUDE.md Normal file
View File

@@ -0,0 +1,80 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with
code in this repository.
## Project Overview
macOS Emacs building system with Ruby and Go components:
- **Ruby script** (`build-emacs-for-macos`): Main build script for creating
self-contained Emacs.app bundles
- **Go CLI tool** (`emacs-builder`): Packaging, signing, notarization, and
release management
- **Dual dependency management**: Nix (preferred) or Homebrew
## Common Commands
```bash
# Environment setup (Nix preferred)
nix develop # Default macOS 11 SDK
nix develop .#macos{11-15,26} # Target specific SDK version
# Go development
make build # Build emacs-builder CLI
make test # Run tests with race detection
make lint # Run golangci-lint
make format # Format with gofumpt
# Ruby development
bundle exec rubocop # Lint (with development group)
# Build Emacs
./build-emacs-for-macos # Build from master
./build-emacs-for-macos emacs-29.4
```
## Architecture
### Ruby Build Script (`build-emacs-for-macos`)
Single-file Ruby script (~2500 lines) that:
- Downloads source tarballs from emacs-mirror/emacs on GitHub
- Configures and compiles Emacs with native-comp, tree-sitter support
- Creates self-contained .app bundles by embedding/relinking dependencies
- Uses `ruby-macho` gem for Mach-O binary manipulation (RPATH handling)
### Go CLI (`cmd/emacs-builder/`)
Uses `urfave/cli/v2` framework. Key packages in `pkg/`:
- `cli/`: Commands (plan, sign, sign-files, notarize, package, release, cask)
- `sign/`: macOS code signing via `codesign`
- `notarize/`: Apple notarization workflow via `notarytool`
- `release/`: GitHub release management
- `dmgbuild/`: DMG creation using Python dmgbuild
- `plan/`: Build plan JSON parsing and management
### Nix Environment (`flake.nix`)
- Multi-SDK support: macOS 11-15, 26 via `.#macos{11,12,13,14,15,26}`
- Excludes ncurses intentionally (links against system version for TUI)
- Sets `MACOSX_DEPLOYMENT_TARGET`, `DEVELOPER_DIR`, `NIX_LIBGCCJIT_*`
## Testing
```bash
make test # All Go tests
go test ./pkg/release/... # Single package
go test -run TestName ./pkg/... # Single test
```
Tests use `_test.go` suffix alongside source files.
## Working Directories
- `sources/`: Downloaded/extracted Emacs source (gitignored)
- `builds/`: Build outputs and .app bundles (gitignored)
- `patches/`: Emacs patches applied during build
- `bin/`: Built Go binaries (gitignored)

View File

@@ -1,6 +1,6 @@
# frozen_string_literal: true
source 'http://rubygems.org/'
source 'https://rubygems.org/'
gem 'ruby-macho'

View File

@@ -1,5 +1,5 @@
GEM
remote: http://rubygems.org/
remote: https://rubygems.org/
specs:
ast (2.4.2)
json (2.8.2)

View File

@@ -71,7 +71,7 @@ $(TOOLDIR)/$(1): Makefile
endef
$(eval $(call tool,gofumpt,mvdan.cc/gofumpt@latest))
$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/cmd/golangci-lint@v1.61))
$(eval $(call tool,golangci-lint,github.com/golangci/golangci-lint/v2/cmd/golangci-lint@v2.1))
$(eval $(call tool,gomod,github.com/Helcaraxan/gomod@latest))
.PHONY: tools
@@ -141,10 +141,10 @@ nix-flake-update:
.SILENT: flake-package-versions
flake-package-versions:
nix develop --command -- bash -c \
nix develop --command bash -c \
'nix derivation show \
$$(echo $$PATH | tr ":" "\n" | grep "/nix/store" | sort -u) \
| jq -r ".[].name" | sort -u'
| jq -r ".derivations[].name" | sort -u'
flake.pkgs: flake.nix flake.lock
$(MAKE) flake-package-versions > "$@"

View File

@@ -141,7 +141,7 @@ Options:
--[no-]relink-eln-files Enable/disable re-linking shared libraries in bundled *.eln files (default: enabled)
--[no-]rsvg Enable/disable SVG image support via librsvg (default: enabled)
--[no-]dbus Enable/disable dbus support (default: enabled)
--[no-]alpha-background Enable/disable experimental alpha-background patch when building Emacs 30.x - 31.x (default: disabled)
--alpha-background Apply experimental alpha-background patch when building Emacs 30.x - 31.x (default: disabled)
--no-frame-refocus Apply no-frame-refocus patch when building Emacs 27.x - 31.x (default: disabled)
--no-titlebar Apply no-titlebar patch when building Emacs 27.x - 28.x (default: disabled)
--[no-]xwidgets Enable/disable XWidgets when building Emacs 27.x (default: disabled)
@@ -155,12 +155,16 @@ Options:
-o, --output DIR Output directory for finished builds (default: <work-dir>/builds)
--build-name NAME Override generated build name
--dist-include x,y,z List of extra files to copy from Emacs source into build folder/archive (default: COPYING)
--icon-uri URI Local path or URL to a .icns file to replace the default app icon
--tahoe-icon-uri URI Local path or URL to an Assets.car file for macOS 26 icons. Requires --tahoe-icon-name.
--tahoe-icon-name NAME Name of the icon in Assets.car to set as CFBundleIconName
--[no-]self-sign Enable/disable self-signing of Emacs.app (default: enabled)
--[no-]archive Enable/disable creating *.tbz archive (default: enabled)
--[no-]archive-keep-build-dir
Enable/disable keeping source folder for archive (default: disabled)
--log-level LEVEL Build script log level (default: info)
--plan FILE Follow given plan file, instead of using given git ref/sha
--clean-macho-binary FILE Tool to clean duplicate RPATHs from given Mach-O binary.
```
Resulting applications are saved to the `builds` directory in a bzip2 compressed

View File

@@ -12,6 +12,7 @@ require 'net/http'
require 'open3'
require 'optparse'
require 'pathname'
require 'set'
require 'time'
require 'tmpdir'
require 'uri'
@@ -228,6 +229,14 @@ class Build
handle_native_lisp(app)
if options[:icon_uri] || options[:tahoe_icon_uri] || options[:tahoe_icon_name]
IconEmbedder.new(
app,
icon_uri: options[:icon_uri],
tahoe_icon_uri: options[:tahoe_icon_uri],
tahoe_icon_name: options[:tahoe_icon_name]
).embed
end
CLIHelperEmbedder.new(app).embed
CSourcesEmbedder.new(app, @source_dir).embed
LibEmbedder.new(
@@ -506,7 +515,7 @@ class Build
def env_CFLAGS
return @env_CFLAGS if @env_CFLAGS
env = []
env = ENV.fetch('CFLAGS', nil)&.split || []
env << '-O2'
@@ -532,13 +541,28 @@ class Build
env += ENV['NIX_CFLAGS_COMPILE'].split
end
@env_CFLAGS = env
# Group "-isystem <path>" flags together as a single flag. This allows us to
# de-duplicate CFLAGS from NIX_CFLAGS_COMPILE.
new_env = []
isystem_flag = false
env.each do |flag|
if flag.strip == '-isystem'
isystem_flag = true
elsif isystem_flag
new_env << "-isystem #{flag}"
isystem_flag = false
else
new_env << flag
end
end
@env_CFLAGS = new_env.compact.reject(&:empty?).uniq
end
def env_LDFLAGS
return @env_LDFLAGS if @env_LDFLAGS
env = []
env = ENV.fetch('LDFLAGS', nil)&.split || []
# Ensure library re-linking and code signing will work after building.
env << '-Wl,-headerpad_max_install_names'
@@ -555,13 +579,13 @@ class Build
env += ENV['NIX_LDFLAGS'].split if use_nix? && ENV['NIX_LDFLAGS']
@env_LDFLAGS = env
@env_LDFLAGS = env.compact.reject(&:empty?).uniq
end
def env_LIBRARY_PATH
return @env_LIBRARY_PATH if @env_LIBRARY_PATH
env = []
env = ENV.fetch('LIBRARY_PATH', nil)&.split || []
if options[:native_comp]
env += [
@@ -573,37 +597,45 @@ class Build
env << '/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib'
@env_LIBRARY_PATH = env
@env_LIBRARY_PATH = env.compact.reject(&:empty?).uniq
end
def env_PKG_CONFIG_PATH
return [] if use_nix?
env = ENV.fetch('PKG_CONFIG_PATH', nil)&.split || []
@env_PKG_CONFIG_PATH ||= [
File.join(brew_dir, 'lib/pkgconfig'),
File.join(brew_dir, 'share/pkgconfig'),
File.join(brew_dir, 'opt/expat/lib/pkgconfig'),
File.join(brew_dir, 'opt/libxml2/lib/pkgconfig'),
File.join(brew_dir, 'opt/ncurses/lib/pkgconfig'),
File.join(brew_dir, 'opt/zlib/lib/pkgconfig'),
File.join(
brew_dir,
'Homebrew/Library/Homebrew/os/mac/pkgconfig',
OS.version.to_s
)
]
return env if use_nix?
@env_PKG_CONFIG_PATH = (
[
File.join(brew_dir, 'lib/pkgconfig'),
File.join(brew_dir, 'share/pkgconfig'),
File.join(brew_dir, 'opt/expat/lib/pkgconfig'),
File.join(brew_dir, 'opt/libxml2/lib/pkgconfig'),
File.join(brew_dir, 'opt/ncurses/lib/pkgconfig'),
File.join(brew_dir, 'opt/zlib/lib/pkgconfig'),
File.join(
brew_dir,
'Homebrew/Library/Homebrew/os/mac/pkgconfig',
OS.version.to_s
)
] + env
).compact.reject(&:empty?).uniq
end
def env_PATH
return [] if use_nix?
env = ENV.fetch('PATH', nil)&.split || []
@env_PATH ||= [
File.join(brew_dir, 'opt/make/libexec/gnubin'),
File.join(brew_dir, 'opt/coreutils/libexec/gnubin'),
File.join(brew_dir, 'opt/gnu-sed/libexec/gnubin'),
File.join(brew_dir, 'bin'),
File.join(brew_dir, 'opt/texinfo/bin')
]
return env if use_nix?
@env_PATH = (
[
File.join(brew_dir, 'opt/make/libexec/gnubin'),
File.join(brew_dir, 'opt/coreutils/libexec/gnubin'),
File.join(brew_dir, 'opt/gnu-sed/libexec/gnubin'),
File.join(brew_dir, 'bin'),
File.join(brew_dir, 'opt/texinfo/bin')
] + env
).compact.reject(&:empty?).uniq
end
# rubocop:enable Naming/MethodName,Naming/VariableName
@@ -612,22 +644,14 @@ class Build
env = {
'CC' => use_nix? ? 'clang' : '/usr/bin/clang',
'PATH' => [
env_PATH, ENV.fetch('PATH', nil)
].flatten.compact.reject(&:empty?).join(':'),
'PKG_CONFIG_PATH' => [
env_PKG_CONFIG_PATH,
ENV.fetch('PKG_CONFIG_PATH', nil)
].flatten.compact.reject(&:empty?).join(':')
'PATH' => env_PATH.join(':'),
'PKG_CONFIG_PATH' => env_PKG_CONFIG_PATH.join(':')
}
if options[:native_comp]
env['CFLAGS'] = [env_CFLAGS, ENV.fetch('CFLAGS', nil)]
.flatten.compact.reject(&:empty?).join(' ')
env['LDFLAGS'] = [env_LDFLAGS, ENV.fetch('LDFLAGS', nil)]
.flatten.compact.reject(&:empty?).join(' ')
env['LIBRARY_PATH'] = [env_LIBRARY_PATH, ENV.fetch('LIBRARY_PATH', nil)]
.flatten.compact.reject(&:empty?).join(':')
env['CFLAGS'] = env_CFLAGS.join(' ')
env['LDFLAGS'] = env_LDFLAGS.join(' ')
env['LIBRARY_PATH'] = env_LIBRARY_PATH.join(':')
end
@compile_env = env
@@ -1057,7 +1081,7 @@ class Build
# Enabled by default patches.
if (26..31).include?(effective_version)
if (26..30).include?(effective_version)
p << {
url:
'https://github.com/d12frosted/homebrew-emacs-plus/raw/master/' \
@@ -1065,6 +1089,19 @@ class Build
}
end
# The fix-window-role patch was merged into Emacs 31 on 2025-07-31 with
# commit 6e1054a40bf6df1429a2b16fdd0d7652dae4d537. Hence builds for commits
# before then need the patch from the last commit in emacs-plus before it
# was removed.
if effective_version == 31 && meta[:date] < Time.parse('2025-07-31')
p << {
url:
'https://github.com/d12frosted/homebrew-emacs-plus/raw/' \
'3e95d573d5f13aba7808193b66312b38a7c66851/' \
'patches/emacs-31/fix-window-role.patch'
}
end
if (27..31).include?(effective_version)
p << {
url:
@@ -1384,6 +1421,98 @@ class CSourcesEmbedder < AbstractEmbedder
end
end
class IconEmbedder < AbstractEmbedder
include Helpers
def initialize(app, icon_uri: nil, tahoe_icon_uri: nil, tahoe_icon_name: nil)
super(app)
@icon_uri = icon_uri
@tahoe_icon_uri = tahoe_icon_uri
@tahoe_icon_name = tahoe_icon_name
end
def embed
handle_icns if present?(@icon_uri)
handle_tahoe if present?(@tahoe_icon_uri)
ensure
cleanup_tmpdir
end
private
def present?(val)
!val.nil? && !val.strip.empty?
end
def handle_icns
source = resolve_source(@icon_uri, '.icns', 'icon.icns')
target = File.join(resources_dir, 'Emacs.icns')
info 'Replacing application icon (Emacs.icns)...'
run_cmd('cp', '-pRL', source, target)
source
end
def handle_tahoe
fatal '--tahoe-icon-name is required with --tahoe-icon-uri' \
unless present?(@tahoe_icon_name)
source = resolve_source(@tahoe_icon_uri, '.car', 'Assets.car')
target = File.join(resources_dir, 'Assets.car')
info 'Placing Tahoe Assets.car into Resources...'
run_cmd('cp', '-pRL', source, target)
set_cf_bundle_icon_name(@tahoe_icon_name)
source
end
def set_cf_bundle_icon_name(name)
info 'Setting CFBundleIconName in Info.plist...'
info_plist = File.join(app, 'Contents', 'Info.plist')
fatal "Info.plist not found: #{info_plist}" unless File.exist?(info_plist)
# Use plutil which adds/replaces the key as needed
run_cmd(
'plutil', '-replace', 'CFBundleIconName', '-string',
name, info_plist
)
end
def resolve_source(uri, expected_ext, download_name)
file_path = if valid_url?(uri)
download_file(uri, download_name)
else
local = File.expand_path(uri)
unless File.exist?(local)
fatal "File does not exist: #{local}"
end
local
end
ext = File.extname(file_path).downcase
fatal "Unexpected file type: #{ext} (expected #{expected_ext})" \
unless ext == expected_ext
file_path
end
def tmpdir
@tmpdir ||= Dir.mktmpdir(%w[emacs-assets .tmp])
end
def download_file(url, name)
path = File.join(tmpdir, name)
info "Downloading asset from: #{url}"
run_cmd('curl', '-L#', url, '-o', path)
path
end
def cleanup_tmpdir
return unless @tmpdir
FileUtils.rm_rf(@tmpdir)
end
end
class LibEmbedder < AbstractEmbedder
attr_reader :lib_sources
attr_reader :extra_libs
@@ -1701,6 +1830,7 @@ end
class GccInfo
include Output
include System
def initialize(use_nix: false)
@use_nix = use_nix
@@ -1823,7 +1953,7 @@ class GccInfo
Dir[
File.join(libgccjit_root_dir, 'lib/gcc/*/libgccjit*.dylib'),
File.join(libgccjit_root_dir, 'lib/gcc/*/libgccjit.so*'),
]
]
.map { |path| File.dirname(path) }
.select { |path| File.basename(path).match(/^\d+$/) }
.max_by { |path| File.basename(path).to_i }
@@ -1840,10 +1970,15 @@ class GccInfo
'brew reinstall libgccjit'
end
# No need to verify gcc vs libgccjit for Nix, as we can pull everything we
# need from the libgccjit package. On homebrew we need to pull parts from
# gcc and parts from libgccjit, hence we need to ensure versions match.
return if use_nix?
if use_nix?
Dir[File.join(libgccjit_lib_dir, 'libgccjit*.dylib')]
.each { |path| clean_macho_binary(path) }
# No need to verify gcc vs libgccjit for Nix, as we can pull everything we
# need from the libgccjit package. On homebrew we need to pull parts from
# gcc and parts from libgccjit, hence we need to ensure versions match.
return
end
return if major_version == libgccjit_major_version
@@ -1868,6 +2003,190 @@ class GccInfo
def relative_path(base, path)
Pathname.new(path).relative_path_from(Pathname.new(base)).to_s
end
def clean_macho_binary(path)
debug "Checking for duplicate RPATHs in #{path}"
macho_cleaner = MachOCleaner.new(path)
return unless macho_cleaner.duplicate_rpaths?
begin
info "Removing duplicate RPATHs from #{path}"
macho_cleaner.clean!
debug 'Cleaned duplicate RPATHs successfully!'
rescue MachOCleaner::PermissionError => e
warn "Could not remove duplicate RPATHs from #{path}: #{e.message}"
if ENV['USER'] == 'root'
fatal "Could not remove duplicate RPATHs from #{path}: #{e.message}"
else
warn '================================================================='
warn "Attempting to clean duplicate RPATHs from #{path} as root"
warn '================================================================='
run_cmd('sudo', $PROGRAM_NAME, '--clean-macho-binary', path)
end
end
end
end
# MachOCleaner is a class that cleans up a Mach-O file by removing all duplicate
# RPATH load commands. This ensures compatibility with macOS 15.4 and later,
# which refuses to load binaries and shared libraries with duplicate RPATHs.
class MachOCleaner
include Output
include System
class PermissionError < StandardError
def initialize(file, message = nil)
@file = file
super(message || "Insufficient permissions to modify #{file}")
end
attr_reader :file
end
attr_reader :file
def initialize(file_path, backup: true)
@file = file_path
@backup = backup
validate_file!
end
def backup?
@backup
end
# Main cleaning method - removes duplicate RPATH commands
def clean!
duplicate_paths = find_duplicate_rpaths(macho_object)
return if duplicate_paths.empty?
backup_file! if backup?
while_writable(@file) do
duplicate_paths.each do |rpath|
remove_rpath_with_install_name_tool!(rpath)
end
end
end
# Check if file has duplicate RPATH commands
def duplicate_rpaths?
count_duplicate_rpaths(macho_object).positive?
end
# Return total number of RPATH commands
def rpath_count
count_rpaths(macho_object)
end
# Return number of duplicate RPATH commands
def duplicate_rpath_count
count_duplicate_rpaths(macho_object)
end
private
# Validate that the file exists and is readable
def validate_file!
fatal "File does not exist: #{@file}" unless File.exist?(@file)
return if File.readable?(@file)
fatal "File is not readable: #{@file}"
end
# Load and memoize the Mach-O object
def macho_object
return @macho_object if @macho_object
begin
@macho_object = MachO.open(@file)
rescue MachO::MachOError => e
fatal "Not a valid Mach-O file: #{@file} (#{e.message})"
end
unless @macho_object.respond_to?(:rpaths)
fatal "Unsupported Mach-O file type: #{@file}"
end
@macho_object
end
def backup_file!
backup_file = "#{@file}.bak"
if File.exist?(backup_file)
debug "Backup file already exists: #{backup_file}"
return
end
FileUtils.cp(@file, backup_file)
debug "Backed up #{@file} to #{backup_file}"
rescue Errno::EPERM, Errno::EACCES => e
raise PermissionError.new(
backup_file, "Cannot create backup file: #{e.message}"
)
end
# Temporarily make file writable, execute block, then restore permissions
def while_writable(file)
# Check if file is already writable to avoid unnecessary permission changes
if File.writable?(file)
yield
return
end
original_mode = File.stat(file).mode
begin
File.chmod(0o755, file)
rescue Errno::EPERM, Errno::EACCES => e
raise PermissionError.new(
file, "Cannot change file permissions: #{e.message}"
)
end
yield
ensure
if File.exist?(file) && original_mode
begin
File.chmod(original_mode, file)
rescue Errno::EPERM, Errno::EACCES
# Log warning but don't fail - file was already modified successfully
warn "Warning: Could not restore original permissions for #{file}"
end
end
end
# Find duplicate RPATH commands in a Mach-O file
def find_duplicate_rpaths(macho_file)
seen = Set.new
duplicates = []
macho_file.rpaths.each do |rpath|
if seen.include?(rpath)
duplicates << rpath
else
seen.add(rpath)
end
end
duplicates
end
# Remove an RPATH using install_name_tool
def remove_rpath_with_install_name_tool!(rpath)
run_cmd('install_name_tool', '-delete_rpath', rpath, @file)
end
# Count total RPATH commands in a Mach-O file
def count_rpaths(macho_file)
macho_file.rpaths.size
end
# Count duplicate RPATH commands in a Mach-O file
def count_duplicate_rpaths(macho_file)
find_duplicate_rpaths(macho_file).size
end
end
class CLIOptions
@@ -1882,6 +2201,14 @@ class CLIOptions
def parse!(args)
parser.parse!(args)
if options[:tahoe_icon_uri] &&
(
options[:tahoe_icon_name].nil? ||
options[:tahoe_icon_name].strip.empty?
)
fatal '--tahoe-icon-name is required when --tahoe-icon-uri is specified'
end
rescue OptionParser::InvalidOption => e
fatal e.message
end
@@ -1908,11 +2235,15 @@ class CLIOptions
github_src_repo: nil,
github_auth: true,
dist_include: ['COPYING', 'configure_output.txt'],
icon_uri: nil,
tahoe_icon_uri: nil,
tahoe_icon_name: nil,
self_sign: true,
archive: true,
archive_keep: false,
patches: [],
log_level: 'info'
log_level: 'info',
clean_macho_binary: nil
}
end
@@ -2103,6 +2434,22 @@ class CLIOptions
'folder/archive (default: COPYING)'
) { |v| options[:dist_include] = v }
opts.on(
'--icon-uri URI',
'Local path or URL to a .icns file to replace the default app icon'
) { |v| options[:icon_uri] = v }
opts.on(
'--tahoe-icon-uri URI',
'Local path or URL to an Assets.car file for macOS 26 icons. ' \
'Requires --tahoe-icon-name.'
) { |v| options[:tahoe_icon_uri] = v }
opts.on(
'--tahoe-icon-name NAME',
'Name of the icon in Assets.car to set as CFBundleIconName'
) { |v| options[:tahoe_icon_name] = v }
opts.on(
'--[no-]self-sign',
'Enable/disable self-signing of Emacs.app (default: enabled)'
@@ -2128,6 +2475,11 @@ class CLIOptions
'--plan FILE',
'Follow given plan file, instead of using given git ref/sha'
) { |v| options[:plan] = v }
opts.on(
'--clean-macho-binary FILE',
'Tool to clean duplicate RPATHs from given Mach-O binary.'
) { |v| options[:clean_macho_binary] = v }
end
end
end
@@ -2144,6 +2496,17 @@ if __FILE__ == $PROGRAM_NAME
build.print_info
elsif cli_options[:preview]
build.print_preview
elsif cli_options[:clean_macho_binary]
macho_cleaner = MachOCleaner.new(cli_options[:clean_macho_binary])
if macho_cleaner.duplicate_rpaths?
build.info 'Removing duplicate RPATHs from ' \
"#{cli_options[:clean_macho_binary]}..."
macho_cleaner.clean!
build.info 'Cleaned duplicate RPATHs successfully!'
else
build.info 'No duplicate RPATHs found.'
end
else
build.build
end

8
flake.lock generated
View File

@@ -20,16 +20,16 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1732981179,
"narHash": "sha256-F7thesZPvAMSwjRu0K8uFshTk3ZZSNAsXTIFvXBT+34=",
"lastModified": 1767313136,
"narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "62c435d93bf046a5396f3016472e8f7c8e2aed65",
"rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-24.11",
"ref": "nixos-25.05",
"repo": "nixpkgs",
"type": "github"
}

164
flake.nix
View File

@@ -2,88 +2,104 @@
description = "Development environment flake";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
outputs =
{
self,
nixpkgs,
flake-utils,
}:
flake-utils.lib.eachDefaultSystem (
system:
let
pkgs = nixpkgs.legacyPackages.${system};
# List of supported macOS SDK versions.
sdk_versions = [ "11" "12" "13" "14" "15" ];
sdk_versions = [
"11"
"12"
"13"
"14"
"15"
"26"
];
default_sdk_version = "11";
mkDevShell = { macos_version ? default_sdk_version }:
mkDevShell =
{
macos_version ? default_sdk_version,
}:
let
apple_sdk = pkgs.${"apple-sdk_${macos_version}"};
in
pkgs.mkShell {
# Package list specifically excludes ncurses, so that we link
# against the system version of ncurses. This ensures emacs' TUI
# works out of the box without the user having to manually set
# TERMINFO in the shell before launching emacs.
packages = with pkgs; [
apple_sdk
autoconf
bash
cairo
clang
coreutils
curl
darwin.DarwinTools # sw_vers
dbus
expat
findutils
gcc
gettext
giflib
git
gmp
gnumake
gnupatch
gnused
gnutar
gnutls
harfbuzz
jansson
jq
lcms2
libffi
libgccjit
libiconv
libjpeg
libpng
librsvg
libtasn1
libunistring
libwebp
libxml2
mailutils
nettle
pkg-config
python3
rsync
ruby_3_3
sqlite
texinfo
time
tree-sitter
which
xcbuild
zlib
];
pkgs.mkShell {
# Package list specifically excludes ncurses, so that we link
# against the system version of ncurses. This ensures emacs' TUI
# works out of the box without the user having to manually set
# TERMINFO in the shell before launching emacs.
packages = with pkgs; [
apple_sdk
autoconf
bash
cairo
clang
coreutils
curl
darwin.DarwinTools # sw_vers
dbus
expat
findutils
gcc
gettext
giflib
git
gmp
gnumake
gnupatch
gnused
gnutar
gnutls
harfbuzz
jansson
jq
lcms2
libffi
libgccjit
libiconv
libjpeg
libpng
librsvg
libtasn1
libunistring
libwebp
libxml2
mailutils
nettle
pkg-config
python313Packages.dmgbuild
rsync
ruby
sqlite
texinfo
time
tree-sitter
which
xcbuild
zlib
];
shellHook = ''
export CC=clang
export MACOSX_DEPLOYMENT_TARGET="${macos_version}.0"
export DEVELOPER_DIR="${apple_sdk}"
export NIX_LIBGCCJIT_VERSION="${pkgs.libgccjit.version}"
export NIX_LIBGCCJIT_ROOT="${pkgs.libgccjit.outPath}"
export BUNDLE_WITHOUT=development
'';
};
shellHook = ''
export CC=clang
export MACOSX_DEPLOYMENT_TARGET="${macos_version}.0"
export DEVELOPER_DIR="${apple_sdk}"
export NIX_LIBGCCJIT_VERSION="${pkgs.libgccjit.version}"
export NIX_LIBGCCJIT_ROOT="${pkgs.libgccjit.outPath}"
export BUNDLE_WITHOUT=development
'';
};
# Generate an attrset of shells for each macOS SDK version.
versionShells = builtins.listToAttrs (
@@ -93,10 +109,10 @@
}) sdk_versions
);
in
{
devShells = versionShells // {
default = mkDevShell {};
};
}
{
devShells = versionShells // {
default = mkDevShell { };
};
}
);
}

View File

@@ -1,67 +1,72 @@
DarwinTools-1
autoconf-2.72
bash-5.2p37
bash-interactive-5.2p37
brotli-1.1.0
bzip2-1.0.8
cairo-1.18.2
cctools-binutils-darwin-1010.6
cctools-binutils-darwin-wrapper-1010.6
clang-16.0.6
clang-wrapper-16.0.6
coreutils-9.5
curl-8.11.0
clang-19.1.7
clang-wrapper-19.1.7
coreutils-9.7
curl-8.14.1
dbus-1.14.10
diffutils-3.10
expat-2.6.4
diffutils-3.12
expat-2.7.3
file-5.45
findutils-4.10.0
fontconfig-2.15.0
fontconfig-2.16.0
freetype-2.13.3
gawk-5.3.1
gcc-13.3.0
gcc-wrapper-13.3.0
gawk-5.3.2
gcc-14.3.0
gcc-wrapper-14.3.0
gdk-pixbuf-2.42.12
gettext-0.21.1
gettext-0.22.5
giflib-5.2.2
git-2.47.0
glib-2.82.1
git-2.50.1
glib-2.84.3
gnugrep-3.11
gnumake-4.4.1
gnused-4.9
gnutar-1.35
gnutls-3.8.6
gnutls-3.8.9
graphite2-1.3.14
gzip-1.13
harfbuzz-10.0.1
gzip-1.14
harfbuzz-10.2.0
jq-1.7.1
krb5-1.21.3
lcms2-2.16
libdeflate-1.22
libgccjit-13.3.0
libiconv-107
libidn2-2.3.7
lcms2-2.17
libdeflate-1.23
libgccjit-14.3.0
libiconv-1.17
libiconv-109
libidn2-2.3.8
libjpeg-turbo-3.0.4
libpng-apng-1.6.43
libpng-apng-1.6.46
libpsl-0.21.5
librsvg-2.58.3
libtasn1-4.19.0
librsvg-2.60.0
libtasn1-4.20.0
libtiff-4.7.0
libwebp-1.4.0
libxml2-2.13.4
mailutils-3.17
nettle-3.10
nghttp2-1.64.0
openssl-3.3.2
libwebp-1.5.0
libxml2-2.13.8
mailutils-3.18
nettle-3.10.1
nghttp2-1.65.0
openssl-3.4.3
patch-2.7.6
pkg-config-wrapper-0.29.2
python3-3.12.7
rsync-3.3.0
ruby-3.3.5
sqlite-3.46.1
texinfo-7.1.1
python3-3.13.5
python3.13-dmgbuild-1.6.2
python3.13-ds-store-1.3.1
python3.13-mac-alias-2.2.2
rsync-3.4.1
ruby-3.3.9
sqlite-3.48.0
texinfo-7.2
time-1.9
tree-sitter-0.24.3
which-2.21
tree-sitter-0.25.3
which-2.23
xcbuild-0.1.1-unstable-2019-11-20
xz-5.6.3
zstd-1.5.6
xz-5.8.1
zstd-1.5.7

View File

@@ -287,7 +287,7 @@ func (s *Updater) createRepoFile(
s.logger.Info(
"new commit created",
"commit", contResp.GetSHA(), "message", contResp.GetMessage(),
"url", contResp.Commit.GetHTMLURL(),
"url", contResp.GetHTMLURL(),
)
return nil
@@ -354,7 +354,7 @@ func (s *Updater) updateRepoFile(
s.logger.Info(
"new commit created",
"commit", contResp.GetSHA(), "message", contResp.GetMessage(),
"url", contResp.Commit.GetHTMLURL(),
"url", contResp.GetHTMLURL(),
)
return true, nil

View File

@@ -38,6 +38,10 @@ func planCmd() *cli2.Command {
Name: "sha",
Usage: "override commit SHA of specified git branch/tag",
},
&cli2.IntFlag{
Name: "build-variant",
Usage: "build variant to add to the end of the version string",
},
&cli2.StringFlag{
Name: "format",
Aliases: []string{"f"},
@@ -90,6 +94,7 @@ func planAction(c *cli2.Context, opts *Options) error {
EmacsRepo: c.String("emacs-repo"),
Ref: ref,
SHAOverride: c.String("sha"),
BuildVariant: c.Int("build-variant"),
OutputDir: c.String("output-dir"),
TestBuild: c.String("test-build"),
TestBuildType: plan.Prerelease,

View File

@@ -65,10 +65,7 @@ func (s *OSInfo) distinctVersion(version string) string {
return parts[0]
}
max := len(parts)
if max > 2 {
max = 2
}
end := min(len(parts), 2)
return strings.Join(parts[0:max], ".")
return strings.Join(parts[0:end], ".")
}

View File

@@ -35,6 +35,7 @@ type Options struct {
EmacsRepo string
Ref string
SHAOverride string
BuildVariant int
OutputDir string
TestBuild string
TestBuildType TestBuildType
@@ -95,6 +96,12 @@ func Create(ctx context.Context, opts *Options) (*Plan, error) { //nolint:funlen
releaseName = "Emacs." + version
}
if opts.BuildVariant != 0 {
variant := strconv.Itoa(opts.BuildVariant)
absoluteVersion += "-" + variant
releaseName += "-" + variant
}
// Attempt to get the macOS SDK version from the environment, if it's not
// available, use the version from the system.
targetMacOSVersion := osInfo.DistinctSDKVersion()
@@ -139,7 +146,7 @@ func Create(ctx context.Context, opts *Options) (*Plan, error) { //nolint:funlen
plan.Build.Name += ".test." + testName
plan.Release.Title = "Test Builds (" + testName + ")"
plan.Release.Name = "test-builds"
plan.Release.Name = "test-builds-" + testName
plan.Release.Prerelease = false
plan.Release.Draft = true

View File

@@ -4,7 +4,6 @@ import (
"errors"
"fmt"
"regexp"
"strings"
)
// Errors
@@ -18,8 +17,9 @@ var (
)
var (
stableVersion = regexp.MustCompile(`^\d+\.\d+(?:[a-z]+)?$`)
stableGitRef = regexp.MustCompile(`^emacs-(\d+\.\d+(?:[a-z]+)?)$`)
stableVersion = regexp.MustCompile(`^\d+\.\d+(?:[a-z]+)?(-\d+)?$`)
pretestVersion = regexp.MustCompile(`-pretest(-\d+)?$`)
stableGitRef = regexp.MustCompile(`^emacs-(\d+\.\d+(?:[a-z]+)?)$`)
)
func VersionToName(version string) (string, error) {
@@ -28,7 +28,7 @@ func VersionToName(version string) (string, error) {
}
if stableVersion.MatchString(version) ||
strings.HasSuffix(version, "-pretest") {
pretestVersion.MatchString(version) {
return "Emacs-" + version, nil
}

View File

@@ -30,6 +30,27 @@ func TestVersionToName(t *testing.T) {
},
want: "Emacs.2021-07-01.1b88404.master",
},
{
name: "nightly with variant",
args: args{
version: "2021-07-01.1b88404.master-1",
},
want: "Emacs.2021-07-01.1b88404.master-1",
},
{
name: "pretest",
args: args{
version: "30.0.93-pretest",
},
want: "Emacs-30.0.93-pretest",
},
{
name: "pretest with variant",
args: args{
version: "30.0.93-pretest-1",
},
want: "Emacs-30.0.93-pretest-1",
},
{
name: "stable",
args: args{
@@ -44,6 +65,20 @@ func TestVersionToName(t *testing.T) {
},
want: "Emacs-23.3b",
},
{
name: "stable with variant",
args: args{
version: "23.3-1",
},
want: "Emacs-23.3-1",
},
{
name: "stable with letter and variant",
args: args{
version: "23.3b-1",
},
want: "Emacs-23.3b-1",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {