Somewhat hacky script to automate building of Emac.app on macOS.
Find a file
Jim Myhrberg 073f9a3134
feat: enable macOS privacy permission prompts (#155)
* feat: add macOS privacy usage descriptions

macOS attributes protected-resource requests from Emacs Lisp and child
processes to Emacs.app. Missing descriptions can prevent the normal consent
prompt or terminate the requesting process.

Embed a compatibility set of child-aware usage descriptions before signing,
cover legacy and current macOS keys, and exercise the resulting plist through
unit and native plutil integration tests.

* fix: clarify file access privacy descriptions

Emacs itself commonly opens files in protected locations, so describing every
file request as originating from a hosted program is misleading.

Name both Emacs and programs running within it for file and folder access while
retaining child-focused wording for typically hosted capabilities.

* fix: keep Nix builds compatible with newer macOS SDKs

Newer Command Line Tools SDK stubs can be incompatible with Nix's older
linker, causing configure probes to fail before compilation.

Expose only a sanitized system ncurses stub in LIBRARY_PATH so Emacs keeps
using the system terminfo database without allowing the host SDK to shadow
Nix libraries. Cover both Nix and non-Nix build environments.

* fix: isolate Ruby and Go CI tests

The combined job mixed two toolchains and ran Ruby without installing its
Gemfile dependencies. Run each suite in its own job so failures are scoped and
the Ruby job installs its bundle before testing.

Migrate the Ruby coverage to RSpec and retain dedicated Make targets for both
suites.

* chore(ci): pin actions to current releases

Floating major-version references allow workflow code to change without a
repository update. Resolve the latest stable releases with pinact and pin each
action to its full commit SHA while retaining readable version comments.

* fix: allow hardened builds to request protected resources

Hardened Runtime blocks privacy-protected APIs before macOS can show a
permission prompt unless the app signature includes matching resource
entitlements.

Add the supported audio input, camera, contacts, calendars, location, and
Photos entitlements to the default Emacs signing configuration.

* fix(ci): avoid persisting checkout credentials

Read-only test and lint jobs do not perform authenticated Git operations, so
retaining the workflow token in their checkouts unnecessarily exposes it to
repository-controlled commands. Disable credential persistence for all four
checkout steps.
2026-08-14 21:54:14 +01:00
.github feat: enable macOS privacy permission prompts (#155) 2026-08-14 21:54:14 +01:00
cmd/emacs-builder feat(cli): add basis for new "emacs-builder" CLI tool written in Go 2021-06-20 18:38:36 +01:00
helper chore(helper): minor tweak to emacs helper launch script 2022-04-30 17:46:06 +01:00
lib feat: enable macOS privacy permission prompts (#155) 2026-08-14 21:54:14 +01:00
patches/emacs-29 feat(patches/alpha-background): add experimental alpha-background patch (#129) 2024-12-07 21:00:41 +00:00
pkg feat: enable macOS privacy permission prompts (#155) 2026-08-14 21:54:14 +01:00
spec feat: enable macOS privacy permission prompts (#155) 2026-08-14 21:54:14 +01:00
.gitignore feat(deps): add support for Nix package manager (#116) 2024-11-25 02:31:47 +00:00
.golangci.yml chore(ci/deps): upgrade CI dependencies and fix linting issues (#136) 2025-06-27 11:25:16 +01:00
.rubocop.yml feat: options for log-level and github source repository 2023-07-31 18:39:38 +01:00
Brewfile chore(deps): add webp to Brewfile 2023-07-31 18:36:22 +01:00
build-emacs-for-macos feat: enable macOS privacy permission prompts (#155) 2026-08-14 21:54:14 +01:00
CHANGELOG.md chore(master): release 0.6.63 (#149) 2025-09-14 21:01:51 +01:00
CLAUDE.md feat: enable macOS privacy permission prompts (#155) 2026-08-14 21:54:14 +01:00
flake.lock chore(deps): update flake.lock and flake.pkgs for dependency versions 2026-01-14 18:50:55 +00:00
flake.nix chore(deps): add macOS SDK version 26 2026-01-14 18:53:56 +00:00
flake.pkgs chore(deps): update flake.lock and flake.pkgs for dependency versions 2026-01-14 18:50:55 +00:00
Gemfile feat: enable macOS privacy permission prompts (#155) 2026-08-14 21:54:14 +01:00
Gemfile.lock feat: enable macOS privacy permission prompts (#155) 2026-08-14 21:54:14 +01:00
go.mod feat(deps): add support for Nix package manager (#116) 2024-11-25 02:31:47 +00:00
go.sum feat(deps): add support for Nix package manager (#116) 2024-11-25 02:31:47 +00:00
Makefile feat: enable macOS privacy permission prompts (#155) 2026-08-14 21:54:14 +01:00
README.md feat: enable macOS privacy permission prompts (#155) 2026-08-14 21:54:14 +01:00
requirements-ci.txt chore(makefile): add bootstrap targets for normal use and CI environments 2021-06-22 00:13:25 +01:00

build-emacs-for-macos

My personal hacked together script for building a completely self-contained Emacs.app application on macOS, from any git branch, tag, or ref. With support for native-compilation.

Use this script at your own risk.

Why?

  • To use new features available from master or branches, which have not made it into a official stable release yet.
  • Homebrew builds of Emacs are not self-contained applications, making it very difficult when doing HEAD builds and you need to rollback to a earlier version.
  • Both Homebrew HEAD builds, and nightly builds from emacsformacosx.com are built from the master branch. This script allows you to choose any branch, tag, or git ref you want.

Binary Builds

Nightly and stable binary builds produced with this build script are available from jimeh/emacs-builds.

Privacy Permissions

Every Emacs.app produced by this project includes macOS privacy usage descriptions for protected resources commonly accessed by Emacs Lisp and child processes. The descriptions cover both legacy and current macOS keys across the supported deployment range and are embedded before the application is signed.

These descriptions allow macOS to explain a permission request and display its normal consent prompt. They do not grant access or bypass the user's privacy settings.

Limitations

The build produced does have some limitations:

  • It is not a universal application. The CPU architecture of the built application will be that of the machine it was built on.
  • The minimum required macOS version of the built application will be the same as that of the machine it was built on.
  • The application is not signed automatically, but the CLI tool used to sign the nightly builds is available. Run go run ./cmd/emacs-builder package --help for details. More detailed instructions will come soon.

Requirements

Required with both Nix and Homebrew approaches:

Nix

The Nix package manager is the preferred and most reliable way to install all dependencies required to build Emacs, by way of a Nix flake included in the project root.

To install all required dependencies within the nix shell, run:

nix develop --command make bootstrap

Homebrew

If you do not have Nix installed, then the alternative way to manage and install build-time dependencies is via Homebrew.

Ruby 3.3.x or later is also needed to execute the build script. Earlier versions may work, but are untested. Simplest way to install a recent Ruby version is via Homebrew:

brew install ruby

And finally, to install all built-time dependencies, run:

make bootstrap

Status

As of writing (2024-11-30) it works for me on my machine and for the nightly builds in jimeh/emacs-builds. Your luck may vary.

I have successfully built:

  • emacs-29.4 release tag.
  • emacs-30.0.92 pretest tag.
  • master branch (Emacs 31.x).

For reference, my machine is:

  • 14-inch MacBook Pro (2023), Apple M3 Max (16-cores)
  • macOS Sonoma 15.1.1 (24B91)
  • Xcode 16.1 (16B40)

The nightly builds are built with GitHub Actions on GitHub-hosted runners, using macos-13 for Intel builds, and macos-14 for Apple Silicon builds. The build environment is managed with Nix, and targets the macOS 11 SDK.

Usage

Nix

Ensure Flakes are enabled, and enter the flake development environment with nix develop. Within this environment, you can execute the ./build-emacs-for-macos --help to get started.

Or you can run the build script via nix develop:

nix develop --command ./build-emacs-for-macos --help

The Nix environment defaults to targeting the macOS 11 SDK, which makes Emacs builds compatible with macOS 11.3 or later. You can easily target later macOS SDKs. Versions 11 to 15 are available. For example, to target the macOS 12 SDK, run nix develop .#macos12

Homebrew

Run make boostrap to ensure all Ruby and Homebrew dependencies are installed.

Build Script

Usage: ./build-emacs-for-macos [options] <branch/tag/sha>

Branch, tag, and SHA are from the emacs-mirror/emacs/emacs Github repo,
available here: https://github.com/emacs-mirror/emacs

Options:
        --info                       Print environment info and detected library paths, then exit
        --preview                    Print preview details about build and exit.
    -j, --parallel COUNT             Compile using COUNT parallel processes (detected: 16)
        --git-sha SHA                Override detected git SHA of specified branch allowing builds of old commits
        --[no-]use-nix               Use Nix instead of Homebrew to find dependencies (default: enabled if IN_NIX_SHELL is set)
        --[no-]tree-sitter           Enable/disable tree-sitter if supported (default: enabled)
        --[no-]native-comp           Enable/disable native-comp (default: enabled if supported)
        --optimize                   Shorthand for --native-march --native-mtune --fomit-frame-pointer (default: disabled)
        --[no-]native-march          Enable/disable -march=native CFLAG (default: disabled)
        --[no-]native-mtune          Enable/disable -mtune=native CFLAG (default: disabled)
        --[no-]fomit-frame-pointer   Enable/disable -fomit-frame-pointer CFLAG (default: disabled)
        --[no-]native-full-aot       Enable/disable NATIVE_FULL_AOT / Ahead of Time compilation (default: disabled)
        --[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)
        --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)
        --[no-]poll                  Apply poll patch (deprecated)
        --posix-spawn                Apply posix-spawn patch (deprecated)
    -p, --patch=URL                  Specify a custom patch file or URL to apply to the Emacs source (can be used multiple times)
        --[no-]fd-setsize SIZE       Set an file descriptor (max open files) limit (default: 10000)
        --github-src-repo REPO       Specify a GitHub repo to download source tarballs from (default: emacs-mirror/emacs)
        --[no-]github-auth           Make authenticated GitHub API requests if GITHUB_TOKEN environment variable is set.(default: enabled)
        --work-dir DIR               Specify a working directory where tarballs, sources, and builds will be stored and worked with
    -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 tarball.

If you don't want the build process to eat all your CPU cores, pass in a -j value of how many CPU cores you want it to use.

Re-building the same Git SHA again can yield weird results unless you first trash the corresponding directory from the sources directory.

Examples

To download a tarball of the master branch (Emacs 28.x with native-compilation as of writing) and build Emacs.app from it:

./build-emacs-for-macos

To build the stable emacs-29.4 release git tag run:

./build-emacs-for-macos emacs-29.4

All sources as downloaded as tarballs from the emacs-mirror GitHub repository. Hence to get a list of tags/branches available to install, simply check said repository.

Use Emacs.app as emacs CLI Tool

Builds come with a custom emacs shell script launcher for use from the command line, located next to emacsclient in Emacs.app/Contents/MacOS/bin.

The custom emacs script makes sure to use the main Emacs.app/Contents/MacOS/Emacs executable from the correct path, ensuring it finds all the relevant dependencies within the Emacs.app bundle, regardless of it it's exposed via PATH or symlinked to from elsewhere.

To use it, simply add Emacs.app/Contents/MacOS/bin to your PATH. For example, if you place Emacs.app in /Applications:

if [ -d "/Applications/Emacs.app/Contents/MacOS/bin" ]; then
  export PATH="/Applications/Emacs.app/Contents/MacOS/bin:$PATH"
  alias emacs="emacs -nw" # Always launch "emacs" in terminal mode.
fi

If you want emacs in your terminal to launch a GUI instance of Emacs, don't use the alias from the above example.

Native-Comp

The build script will automatically detect if the source tree being built supports native-compilation, and enable it if available. You can override the auto-detection logic to force enable or force disable native-compilation by passing --native-comp or --no-native-comp respectively.

By default NATIVE_FULL_AOT is disabled which ensures a fast build by native compiling as few elisp source files as possible to build Emacs itself. Any remaining elisp files will be dynamically compiled in the background the first time they are used.

To enable native full Ahead-of-Time compilation, pass in the --native-full-aot option, which will native-compile all of Emacs' elisp at built-time. On my machine it takes around 10 minutes to build Emacs.app with NATIVE_FULL_AOT disabled, and around 20-25 minutes with it enabled.

Configuration

Native-Lisp Cache Directory

By default natively compiled *.eln files will be cached in ~/.emacs.d/eln-cache/. If you want to customize that, simply set a new path as the first element of the native-comp-eln-load-path variable. The path string must end with a /.

Below is an example which stores all compiled *.eln files in cache/eln-cache within your Emacs configuration directory:

(when (boundp 'native-comp-eln-load-path)
  (setcar native-comp-eln-load-path
          (expand-file-name "cache/eln-cache/" user-emacs-directory)))

Compilation Warnings

By default any warnings encountered during async native compilation will pop up a warnings buffer. As this tends to happen rather frequently with a lot of packages, it can get annoying. You can disable showing these warnings by setting native-comp-async-report-warnings-errors to nil:

(setq native-comp-async-report-warnings-errors nil)

Issues

Please see all issues with the native-comp label. It's a good idea if you read through them so you're familiar with the types of issues and or behavior you can expect.

Known Good Commits/Builds

A list of known "good" commits which produce working builds is tracked in: #6 Known good commits for native-comp

Credits

Internals

The script downloads the source code as a gzipped tar archive from the GitHub mirror repository, as it makes it very easy to get a tarball of any given git reference.

It then runs ./configure with a various options, including copying various dynamic libraries into the application itself. So the built application should in theory run on a macOS install that does not have Homebrew, or does not have the relevant Homebrew formulas installed.

Code quality of the script itself, is well, non-existent. The build script started life a super-quick hack back in 2013, and now it's even more of a dirty hack. I might clean it up and add unit tests if I end up relying on this script for a prolonged period of time. For now I plan to use it at least until native-comp lands in a stable Emacs release for macOS.

License

CC0 1.0 Universal