mirror of
https://github.com/jimeh/build-emacs-for-macos.git
synced 2026-02-19 13:06:38 +00:00
The ref was not correctly set when loading a build plan, resulting in the set of patches being selected were always for Emacs 28.x, preventing builds of Emacs 27.x and 26.x.
1156 lines
31 KiB
Ruby
Executable File
1156 lines
31 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
# frozen_string_literal: true
|
|
|
|
require 'English'
|
|
require 'date'
|
|
require 'erb'
|
|
require 'etc'
|
|
require 'fileutils'
|
|
require 'json'
|
|
require 'net/http'
|
|
require 'optparse'
|
|
require 'pathname'
|
|
require 'time'
|
|
require 'uri'
|
|
require 'yaml'
|
|
|
|
class Error < StandardError; end
|
|
|
|
module Output
|
|
def info(msg, newline: true)
|
|
out "INFO: #{msg}", newline: newline
|
|
end
|
|
|
|
def out(msg, newline: true)
|
|
if newline
|
|
warn "==> #{msg}"
|
|
else
|
|
STDERR.print "==> #{msg}"
|
|
end
|
|
end
|
|
|
|
def err(msg = nil)
|
|
raise Error, msg
|
|
end
|
|
end
|
|
|
|
class OS
|
|
def self.version
|
|
@version ||= OSVersion.new
|
|
end
|
|
|
|
def self.arch
|
|
@arch ||= `uname -m`.strip
|
|
end
|
|
end
|
|
|
|
class OSVersion
|
|
def initialize
|
|
@version = `sw_vers -productVersion`.match(
|
|
/(?<major>\d+)(?:\.(?<minor>\d+)(:?\.(?<patch>\d+))?)?/
|
|
)
|
|
end
|
|
|
|
def to_s
|
|
@to_s ||= "#{major}.#{minor}"
|
|
end
|
|
|
|
def major
|
|
@major ||= @version[:major]&.to_i
|
|
end
|
|
|
|
def minor
|
|
@minor ||= @version[:minor]&.to_i
|
|
end
|
|
|
|
def patch
|
|
@patch ||= @version[:patch]&.to_i
|
|
end
|
|
end
|
|
|
|
class Build
|
|
include Output
|
|
|
|
EMACS_MIRROR_REPO = 'emacs-mirror/emacs'
|
|
DOWNLOAD_URL = 'https://github.com/emacs-mirror/emacs/tarball/%s'
|
|
|
|
attr_reader :root_dir
|
|
attr_reader :source_dir
|
|
attr_reader :ref
|
|
attr_reader :options
|
|
attr_reader :gcc_info
|
|
|
|
def initialize(root_dir, ref = nil, options = {})
|
|
@root_dir = root_dir
|
|
@ref = ref || 'master'
|
|
@options = options
|
|
@gcc_info = GccInfo.new
|
|
end
|
|
|
|
def build
|
|
load_plan(options[:plan]) if options[:plan]
|
|
|
|
unless meta[:sha] && meta[:date]
|
|
err 'Failed to get commit info from GitHub.'
|
|
end
|
|
|
|
tarball = download_tarball(meta[:sha])
|
|
@source_dir = extract_tarball(tarball, patches(options))
|
|
|
|
autogen
|
|
detect_native_comp if options[:native_comp].nil?
|
|
|
|
app = compile_source(@source_dir)
|
|
build_dir, app = create_build_dir(app)
|
|
|
|
handle_native_lisp(app)
|
|
add_cli_helper(app)
|
|
|
|
LibEmbedder.new(app, brew_dir, extra_libs).embed
|
|
GccLibEmbedder.new(app, gcc_info).embed if options[:native_comp]
|
|
|
|
archive_build(build_dir) if options[:archive]
|
|
end
|
|
|
|
private
|
|
|
|
def load_plan(filename)
|
|
plan = YAML.safe_load(File.read(filename), [:Time])
|
|
|
|
@ref = plan.dig('source', 'ref')
|
|
@meta = {
|
|
sha: plan.dig('source', 'commit', 'sha'),
|
|
ref: @ref,
|
|
date: plan.dig('source', 'commit', 'date')
|
|
}
|
|
|
|
if plan.dig('output', 'directory')
|
|
@output_dir = plan.dig('output', 'directory')
|
|
end
|
|
|
|
if plan.dig('output', 'archive')
|
|
@archive_filename = plan.dig('output', 'archive')
|
|
end
|
|
|
|
if plan.dig('output', 'disk_image') || !plan.dig('output', 'archive')
|
|
options[:archive] = false
|
|
end
|
|
|
|
@build_name = plan.dig('build', 'name') if plan.dig('build', 'name')
|
|
end
|
|
|
|
def tarballs_dir
|
|
@tarballs_dir ||= File.join(root_dir, 'tarballs')
|
|
end
|
|
|
|
def sources_dir
|
|
@sources_dir ||= File.join(root_dir, 'sources')
|
|
end
|
|
|
|
def output_dir
|
|
@output_dir ||= (options[:output] || File.join(root_dir, 'builds'))
|
|
end
|
|
|
|
def brew_dir
|
|
@brew_dir ||= `brew --prefix`.chomp
|
|
end
|
|
|
|
def extra_libs
|
|
@extra_libs ||= [
|
|
File.join(brew_dir, 'opt/expat/lib/libexpat.1.dylib'),
|
|
File.join(brew_dir, 'opt/libiconv/lib/libiconv.2.dylib'),
|
|
File.join(brew_dir, 'opt/zlib/lib/libz.1.dylib')
|
|
]
|
|
end
|
|
|
|
def download_tarball(sha)
|
|
FileUtils.mkdir_p(tarballs_dir)
|
|
|
|
url = (DOWNLOAD_URL % sha)
|
|
filename = "emacs-mirror-emacs-#{sha[0..6]}.tgz"
|
|
target = File.join(tarballs_dir, filename)
|
|
|
|
if File.exist?(target)
|
|
info "#{filename} already exists locally, attempting to use."
|
|
return target
|
|
end
|
|
|
|
info 'Downloading tarball from GitHub. This could take a while, ' \
|
|
'please be patient.'
|
|
|
|
args = ['curl', '-L', url, '-o', target]
|
|
log_args = args.clone
|
|
|
|
if options[:github_auth] && ENV['GITHUB_TOKEN']
|
|
args = [args[0]] +
|
|
['-H', "Authorization: Token #{ENV['GITHUB_TOKEN']}"] + args[1..-1]
|
|
log_args = [log_args[0]] +
|
|
['-H', '"Authorization: Token $GITHUB_TOKEN"'] +
|
|
log_args[1..-1]
|
|
end
|
|
|
|
out "CMD: #{log_args.join(' ')}"
|
|
system(*args) || err("Exit code: #{$CHILD_STATUS.exitstatus}")
|
|
|
|
target
|
|
end
|
|
|
|
def extract_tarball(filename, patches = [])
|
|
FileUtils.mkdir_p(sources_dir)
|
|
|
|
dirname = File.basename(filename).gsub(/\.\w+$/, '')
|
|
target = File.join(sources_dir, dirname)
|
|
|
|
if File.exist?(target)
|
|
info "#{dirname} source tree exists, attempting to use."
|
|
return target
|
|
end
|
|
|
|
info 'Extracting tarball...'
|
|
result = run_cmd('tar', '-xzf', filename, '-C', sources_dir)
|
|
err 'Tarball extraction failed.' unless result
|
|
|
|
patches.each { |patch| apply_patch(patch, target) }
|
|
|
|
target
|
|
end
|
|
|
|
def configure_help
|
|
return @configure_help if @configure_help
|
|
|
|
FileUtils.cd(source_dir) { @configure_help = `./configure --help` }
|
|
|
|
@configure_help
|
|
end
|
|
|
|
def supports_xwidgets?
|
|
@supports_xwidgets ||= !!configure_help.match(/\s+--with-xwidgets\s+/)
|
|
end
|
|
|
|
def supports_native_comp?
|
|
@supports_native_comp ||= !native_comp_configure_flag.nil?
|
|
end
|
|
|
|
def native_comp_configure_flag
|
|
@native_comp_configure_flag ||= configure_help.match(
|
|
/\s+(--with-native(?:comp|-compilation))\s+/
|
|
)&.[](1)
|
|
end
|
|
|
|
def detect_native_comp
|
|
info 'Detecting native-comp support: ', newline: false
|
|
options[:native_comp] = supports_native_comp?
|
|
puts options[:native_comp] ? 'Supported' : 'Not supported'
|
|
end
|
|
|
|
def verify_native_comp
|
|
return if supports_native_comp?
|
|
|
|
err 'This emacs source tree does not support native-comp'
|
|
end
|
|
|
|
def autogen
|
|
FileUtils.cd(source_dir) do
|
|
if File.exist?('autogen/copy_autogen')
|
|
run_cmd 'autogen/copy_autogen'
|
|
elsif File.exist?('autogen.sh')
|
|
run_cmd './autogen.sh'
|
|
end
|
|
end
|
|
end
|
|
|
|
def compile_source(source)
|
|
target = File.join(source, 'nextstep')
|
|
emacs_app = File.join(target, 'Emacs.app')
|
|
|
|
if File.exist?(emacs_app)
|
|
info 'Emacs.app already exists in ' \
|
|
"\"#{target.gsub(root_dir + '/', '')}\", attempting to use."
|
|
return emacs_app
|
|
end
|
|
|
|
info 'Compiling from source. This will take a while...'
|
|
|
|
FileUtils.cd(source) do
|
|
if options[:native_comp]
|
|
info 'Compiling with native-comp enabled'
|
|
verify_native_comp
|
|
gcc_info.verify_libgccjit
|
|
|
|
apply_native_comp_env_setup_patch(source)
|
|
|
|
ENV['CFLAGS'] = [
|
|
"-I#{File.join(gcc_info.root_dir, 'include')}",
|
|
"-I#{File.join(gcc_info.libgccjit_root_dir, 'include')}",
|
|
'-O2',
|
|
(options[:native_march] ? '-march=native' : nil),
|
|
ENV['CFLAGS']
|
|
].compact.join(' ')
|
|
|
|
ENV['LDFLAGS'] = [
|
|
"-L#{gcc_info.lib_dir}",
|
|
"-L#{gcc_info.darwin_lib_dir}",
|
|
"-L#{gcc_info.libgccjit_lib_dir}",
|
|
"-I#{File.join(gcc_info.root_dir, 'include')}",
|
|
"-I#{File.join(gcc_info.libgccjit_root_dir, 'include')}",
|
|
ENV['LDFLAGS']
|
|
].compact.join(' ')
|
|
|
|
ENV['LIBRARY_PATH'] = [
|
|
gcc_info.lib_dir,
|
|
gcc_info.darwin_lib_dir,
|
|
gcc_info.libgccjit_lib_dir,
|
|
ENV['LIBRARY_PATH']
|
|
].compact.join(':')
|
|
end
|
|
|
|
ENV['CC'] = 'clang'
|
|
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['PKG_CONFIG_PATH']
|
|
].compact.join(':')
|
|
|
|
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['PATH']
|
|
].compact.join(':')
|
|
|
|
ENV['LIBRARY_PATH'] = [
|
|
ENV['LIBRARY_PATH'],
|
|
'/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib'
|
|
].compact.join(':')
|
|
|
|
configure_flags = [
|
|
'--with-ns',
|
|
'--with-modules',
|
|
'--enable-locallisppath=' \
|
|
'/Library/Application Support/Emacs/${version}/site-lisp:' \
|
|
'/Library/Application Support/Emacs/site-lisp'
|
|
]
|
|
if options[:xwidgets] && supports_xwidgets?
|
|
configure_flags << '--with-xwidgets'
|
|
end
|
|
configure_flags << native_comp_configure_flag if options[:native_comp]
|
|
configure_flags << '--without-rsvg' if options[:rsvg] == false
|
|
|
|
run_cmd './configure', *configure_flags
|
|
|
|
# Disable aligned_alloc on Mojave and below. See issue:
|
|
# https://github.com/daviderestivo/homebrew-emacs-head/issues/15
|
|
if OS.version.major <= 10 && OS.version.minor <= 14
|
|
info 'Force disabling of aligned_alloc on macOS Mojave (10.14.x) ' \
|
|
'and earlier'
|
|
disable_alligned_alloc
|
|
end
|
|
|
|
make_flags = []
|
|
make_flags += ['-j', options[:parallel].to_s] if options[:parallel]
|
|
|
|
if options[:native_comp]
|
|
make_flags << "BYTE_COMPILE_EXTRA_FLAGS=--eval '(setq comp-speed 2)'"
|
|
|
|
if options[:native_full_aot]
|
|
info 'Using NATIVE_FULL_AOT=1'
|
|
make_flags << 'NATIVE_FULL_AOT=1'
|
|
ENV.delete('NATIVE_FAST_BOOT')
|
|
else
|
|
ENV.delete('NATIVE_FULL_AOT')
|
|
ENV['NATIVE_FAST_BOOT'] = '1'
|
|
end
|
|
end
|
|
|
|
run_cmd 'make', *make_flags
|
|
run_cmd 'make', 'install'
|
|
end
|
|
|
|
err 'Build failed.' unless File.exist?(emacs_app)
|
|
|
|
emacs_app
|
|
end
|
|
|
|
def create_build_dir(app)
|
|
app_name = File.basename(app)
|
|
target_dir = File.join(output_dir, build_name)
|
|
|
|
if File.exist?(target_dir)
|
|
err "Output directory #{target_dir} already exists, " \
|
|
'please delete it and try again'
|
|
end
|
|
|
|
info "Copying \"#{app_name}\" to: #{target_dir}"
|
|
|
|
FileUtils.mkdir_p(target_dir)
|
|
FileUtils.cp_r(app, target_dir)
|
|
|
|
options[:dist_include]&.each do |filename|
|
|
src = File.join(source_dir, filename)
|
|
if File.exist?(src)
|
|
info "Copying \"#{filename}\" to: #{target_dir}"
|
|
FileUtils.cp_r(src, target_dir)
|
|
else
|
|
info "Warning: #{filename} does not exist in #{source_dir}"
|
|
end
|
|
end
|
|
|
|
[target_dir, File.join(target_dir, File.basename(app))]
|
|
end
|
|
|
|
def handle_native_lisp(app)
|
|
return unless options[:native_comp]
|
|
|
|
contents_dir = File.join(app, 'Contents')
|
|
|
|
FileUtils.cd(contents_dir) do
|
|
# Skip creation of symlinks if *.eln files are located under
|
|
# Resources/native-lisp. Emacs is capable of finding lisp sources and
|
|
# *.eln cache files without symlinks.
|
|
return if Dir['Resources/native-lisp/**/*.eln'].any?
|
|
|
|
info 'Creating symlinks within Emacs.app needed for native-comp'
|
|
|
|
FileUtils.ln_s('Resources/lisp', 'lisp') unless File.exist?('lisp')
|
|
|
|
source = Dir['MacOS/libexec/emacs/**/eln-cache',
|
|
'MacOS/lib/emacs/**/native-lisp'].first
|
|
|
|
if source.nil?
|
|
err 'Failed to find native-lisp cache directory for symlink creation.'
|
|
end
|
|
|
|
# Check for folder name containing two dots (.), as this causes Apple's
|
|
# codesign CLI tool to fail signing the Emacs.app bundle, complaining with
|
|
# q "bundle format unrecognized" error.
|
|
#
|
|
# The workaround for now is to rename the folder replacing the dots with
|
|
# hyphens (-), and create the native-lisp symlink pointing to the new
|
|
# location.
|
|
if source.match(%r{/.+\..+\..+/})
|
|
# Dig deeper into native-lisp directory
|
|
eln_dir = File.dirname(Dir[File.join(source, '**', '*.eln')].first)
|
|
|
|
base = File.basename(eln_dir)
|
|
parent = File.dirname(eln_dir)
|
|
|
|
until ['.', '/', contents_dir].include?(parent)
|
|
if base.match(/\..+\./)
|
|
old_name = File.join(parent, base)
|
|
new_name = File.join(parent, base.gsub(/\.(.+)\./, '-\\1-'))
|
|
|
|
info "Renaming: #{old_name} --> #{new_name}"
|
|
FileUtils.mv(old_name, new_name)
|
|
end
|
|
|
|
base = File.basename(parent)
|
|
parent = File.dirname(parent)
|
|
end
|
|
|
|
eln_parts = eln_dir.match(
|
|
%r{/(\d+\.\d+\.\d+)/native-lisp/(\d+\.\d+\.\d+-\w+)(?:/.+)?$}i
|
|
)
|
|
if eln_parts
|
|
patch_dump_native_lisp_paths(app, eln_parts[1], eln_parts[2])
|
|
end
|
|
|
|
# Find native-lisp directory again after it has been renamed.
|
|
source = Dir['MacOS/libexec/emacs/**/eln-cache',
|
|
'MacOS/lib/emacs/**/native-lisp'].first
|
|
|
|
if source.nil?
|
|
err 'Failed to find native-lisp cache directory for symlink creation.'
|
|
end
|
|
end
|
|
|
|
target = File.basename(source)
|
|
FileUtils.ln_s(source, target) unless File.exist?(target)
|
|
end
|
|
end
|
|
|
|
def patch_dump_native_lisp_paths(app, emacs_version, eln_version)
|
|
sanitized_emacs_version = emacs_version.gsub('.', '-')
|
|
sanitized_eln_version = eln_version.gsub('.', '-')
|
|
|
|
contents_dir = File.join(app, 'Contents')
|
|
FileUtils.cd(contents_dir) do
|
|
filename = Dir['MacOS/Emacs.pdmp', 'MacOS/libexec/Emacs.pdmp'].first
|
|
err "no Emacs.pdmp file found in #{app}" unless filename
|
|
info 'patching Emacs.pdmp to point at new native-lisp paths'
|
|
|
|
content = File.read(filename, mode: 'rb').gsub(
|
|
"lib/emacs/#{emacs_version}/native-lisp/#{eln_version}/",
|
|
"lib/emacs/#{sanitized_emacs_version}/" \
|
|
"native-lisp/#{sanitized_eln_version}/"
|
|
).gsub(
|
|
"../native-lisp/#{eln_version}/",
|
|
"../native-lisp/#{sanitized_eln_version}/"
|
|
)
|
|
|
|
File.open(filename, 'w') { |f| f.write(content) }
|
|
end
|
|
end
|
|
|
|
def add_cli_helper(app)
|
|
source = File.join(__dir__, 'helper', 'emacs-cli.bash')
|
|
target = File.join(app, 'Contents', 'MacOS', 'bin', 'emacs')
|
|
dir = File.dirname(target)
|
|
|
|
info "Adding \"emacs\" CLI helper to #{dir}"
|
|
|
|
FileUtils.mkdir_p(dir)
|
|
FileUtils.cp(source, target)
|
|
FileUtils.chmod('+w', target)
|
|
end
|
|
|
|
def build_name
|
|
return @build_name if @build_name
|
|
return @build_name = options[:build_name] if options[:build_name]
|
|
|
|
metadata = [
|
|
meta[:date]&.strftime('%Y-%m-%d'),
|
|
meta[:sha][0..6],
|
|
meta[:ref],
|
|
"macOS-#{OS.version}",
|
|
OS.arch
|
|
].compact.map { |v| v.gsub(/[^\w_-]+/, '-') }
|
|
|
|
@build_name = "Emacs.#{metadata.join('.')}"
|
|
end
|
|
|
|
def archive_filename
|
|
@archive_filename ||= File.join(output_dir, "#{build_name}.tbz")
|
|
end
|
|
|
|
def archive_build(build_dir)
|
|
filename = File.basename(archive_filename)
|
|
target_dir = File.dirname(archive_filename)
|
|
|
|
FileUtils.mkdir_p(target_dir)
|
|
|
|
build = File.basename(build_dir)
|
|
parent_dir = File.dirname(build_dir)
|
|
|
|
if !File.exist?(archive_filename)
|
|
info "Creating #{filename} archive in \"#{target_dir}\"..."
|
|
FileUtils.cd(parent_dir) do
|
|
system('tar', '-cjf', archive_filename, build)
|
|
|
|
if options[:archive_keep] == false
|
|
info "Removeing \"#{build}\" directory from #{parent_dir}"
|
|
FileUtils.rm_rf(build_dir)
|
|
end
|
|
end
|
|
else
|
|
info "#{filename} archive exists in " \
|
|
"#{target_dir}, skipping archving."
|
|
end
|
|
end
|
|
|
|
def disable_alligned_alloc
|
|
filename = 'src/config.h'
|
|
content = File.read(filename)
|
|
.gsub('#define HAVE_ALIGNED_ALLOC 1',
|
|
'#undef HAVE_ALIGNED_ALLOC')
|
|
.gsub('#define HAVE_DECL_ALIGNED_ALLOC 1',
|
|
'#undef HAVE_DECL_ALIGNED_ALLOC')
|
|
.gsub('#define HAVE_ALLOCA 1',
|
|
'#undef HAVE_ALLOCA')
|
|
.gsub('#define HAVE_ALLOCA_H 1',
|
|
'#undef HAVE_ALLOCA_H')
|
|
|
|
File.open(filename, 'w') { |f| f.write(content) }
|
|
end
|
|
|
|
def meta
|
|
return @meta if @meta
|
|
|
|
ref_sha = options[:git_sha] || ref
|
|
info "Fetching info for git ref: #{ref_sha}"
|
|
commit_json = github_api_get(
|
|
"/repos/#{EMACS_MIRROR_REPO}/commits/#{ref_sha}"
|
|
)
|
|
err "Failed to get commit info about: #{ref_sha}" if commit_json.nil?
|
|
|
|
commit = JSON.parse(commit_json)
|
|
meta = {
|
|
sha: commit['sha'],
|
|
date: Time.parse(commit['commit']['committer']['date'])
|
|
}
|
|
meta[:ref] = ref if ref && ref[0..6] != meta[:sha][0..6]
|
|
|
|
@meta = meta
|
|
end
|
|
|
|
def github_api_get(uri)
|
|
uri = URI.join('https://api.github.com/', uri)
|
|
|
|
http = Net::HTTP.new(uri.hostname, uri.port)
|
|
http.use_ssl = true if uri.scheme == 'https'
|
|
|
|
request = Net::HTTP::Get.new(uri)
|
|
if options[:github_auth] && ENV['GITHUB_TOKEN']
|
|
request['Authorization'] = "Token #{ENV['GITHUB_TOKEN']}"
|
|
end
|
|
|
|
response = http.request(request)
|
|
return unless response.code == '200'
|
|
|
|
response.body
|
|
end
|
|
|
|
def run_cmd(*args)
|
|
out "CMD: #{args.join(' ')}"
|
|
system(*args) || err("Exit code: #{$CHILD_STATUS.exitstatus}")
|
|
end
|
|
|
|
def apply_native_comp_env_setup_patch(source)
|
|
term = 'native-compile-setup-environment-variables'
|
|
file = 'lisp/emacs-lisp/comp.el'
|
|
return if `grep '#{term}' '#{file}'`.strip.size.positive?
|
|
|
|
template = File.read(
|
|
File.join(__dir__, 'patches/native-comp-env-setup.diff.erb')
|
|
)
|
|
patch = ERB.new(template).result(gcc_info.get_binding)
|
|
patch_file = File.join(source, 'macos_patches/native-comp-env-setup.diff')
|
|
|
|
File.write(patch_file, patch)
|
|
apply_patch({ file: patch_file }, source)
|
|
end
|
|
|
|
def effective_version
|
|
@effective_version ||= begin
|
|
case ref
|
|
when /^emacs-26.*/
|
|
'emacs-26'
|
|
when /^emacs-27.*/
|
|
'emacs-27'
|
|
else
|
|
'emacs-28'
|
|
end
|
|
end
|
|
end
|
|
|
|
def patches(opts = {})
|
|
p = []
|
|
|
|
if %w[emacs-26 emacs-27 emacs-28].include?(effective_version)
|
|
p << {
|
|
url: 'https://github.com/d12frosted/homebrew-emacs-plus/raw/master/' \
|
|
"patches/#{effective_version}/fix-window-role.patch"
|
|
}
|
|
end
|
|
|
|
if %w[emacs-27 emacs-28].include?(effective_version)
|
|
p << {
|
|
url: 'https://github.com/d12frosted/homebrew-emacs-plus/raw/master/' \
|
|
"patches/#{effective_version}/system-appearance.patch"
|
|
}
|
|
|
|
if options[:no_titlebar]
|
|
p << {
|
|
url: 'https://github.com/d12frosted/homebrew-emacs-plus/raw/master/' \
|
|
"patches/#{effective_version}/no-titlebar.patch"
|
|
}
|
|
end
|
|
|
|
if options[:no_frame_refocus]
|
|
p << {
|
|
url: 'https://github.com/d12frosted/homebrew-emacs-plus/raw/master/' \
|
|
"patches/#{effective_version}/no-frame-refocus-cocoa.patch"
|
|
}
|
|
end
|
|
end
|
|
|
|
if effective_version == 'emacs-27'
|
|
p << {
|
|
url: 'https://github.com/d12frosted/homebrew-emacs-plus/raw/master/' \
|
|
"patches/#{effective_version}/ligatures-freeze-fix.patch"
|
|
}
|
|
|
|
if opts[:xwidgets]
|
|
p << {
|
|
url: 'https://github.com/d12frosted/homebrew-emacs-plus/raw/master/' \
|
|
"patches/#{effective_version}/xwidgets_webkit_in_cocoa.patch"
|
|
}
|
|
end
|
|
end
|
|
|
|
p
|
|
end
|
|
|
|
def apply_patch(patch, target)
|
|
err "\"#{target}\" does not exist." unless File.exist?(target)
|
|
|
|
if patch[:file]
|
|
info 'Applying patch...'
|
|
FileUtils.cd(target) { run_cmd('patch', '-f', '-p1', '-i', patch[:file]) }
|
|
elsif patch[:url]
|
|
patch_dir = "#{target}/macos_patches"
|
|
run_cmd('mkdir', '-p', patch_dir)
|
|
|
|
patch_file = File.join(patch_dir, 'patch-{num}.diff')
|
|
num = 1
|
|
while File.exist?(patch_file.gsub('{num}', num.to_s.rjust(3, '0')))
|
|
num += 1
|
|
end
|
|
patch_file = patch_file.gsub('{num}', num.to_s.rjust(3, '0'))
|
|
|
|
info "Downloading patch: #{patch[:url]}"
|
|
run_cmd('curl', '-L#', patch[:url], '-o', patch_file)
|
|
|
|
apply_patch({ file: patch_file }, target)
|
|
elsif patch[:replace]
|
|
err 'Patch replace input error' unless patch[:replace].size == 3
|
|
|
|
file, before, after = patch[:replace]
|
|
filepath = File.join(target, file)
|
|
|
|
err "\"#{file}\" does not exist in #{target}" unless File.exist?(filepath)
|
|
|
|
f = File.open(filepath, 'rb')
|
|
s = f.read
|
|
sub = s.gsub!(before, after)
|
|
err "Replacement filed in #{file}" if sub.nil?
|
|
|
|
f.reopen(filepath, 'wb').write(s)
|
|
f.close
|
|
end
|
|
end
|
|
end
|
|
|
|
class AbstractEmbedder
|
|
include Output
|
|
|
|
attr_reader :app
|
|
|
|
def initialize(app)
|
|
err "#{app} does not exist" unless File.exist?(app)
|
|
|
|
@app = app
|
|
end
|
|
|
|
private
|
|
|
|
def invocation_dir
|
|
File.join(app, 'Contents', 'MacOS')
|
|
end
|
|
|
|
def bin
|
|
File.join(invocation_dir, 'Emacs')
|
|
end
|
|
|
|
def lib_dir
|
|
File.join(invocation_dir, 'lib')
|
|
end
|
|
end
|
|
|
|
class LibEmbedder < AbstractEmbedder
|
|
attr_reader :lib_source
|
|
attr_reader :extra_libs
|
|
|
|
def initialize(app, lib_source, extra_libs = [])
|
|
super(app)
|
|
|
|
@lib_source = lib_source
|
|
@extra_libs = extra_libs
|
|
end
|
|
|
|
def embed
|
|
info 'Embedding libraries into Emacs.app'
|
|
|
|
binary = "#{bin}-bin" if File.exist?("#{bin}-bin")
|
|
binary ||= bin
|
|
|
|
FileUtils.cd(File.dirname(app)) do
|
|
copy_libs(binary)
|
|
copy_extra_libs(extra_libs, binary) if extra_libs.any?
|
|
if eln_files.any?
|
|
info "Embedding libraries for #{eln_files.size} *.eln files " \
|
|
'within Emacs.app'
|
|
rel_path = Pathname.new(lib_dir).relative_path_from(
|
|
Pathname.new(File.dirname(binary))
|
|
).to_s
|
|
eln_files.each { |f| copy_libs(f, rel_path) }
|
|
end
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
def eln_files
|
|
@eln_files ||= Dir[
|
|
File.join(
|
|
app, 'Contents', 'Resources', 'native-lisp', '**', '*.eln'
|
|
),
|
|
File.join(
|
|
app, 'Contents', 'MacOS', 'lib', 'emacs', '**',
|
|
'native-lisp', '**', '*.eln'
|
|
),
|
|
File.join(
|
|
app, 'Contents', 'MacOS', 'libexec', 'emacs', '**',
|
|
'eln-cache', '**', '*.eln'
|
|
)
|
|
]
|
|
end
|
|
|
|
def copy_libs(exe, rel_path = nil)
|
|
exe_file = File.basename(exe)
|
|
rel_path ||= Pathname.new(lib_dir).relative_path_from(
|
|
Pathname.new(File.dirname(exe))
|
|
).to_s
|
|
|
|
rpath = File.join('@executable_path', rel_path)
|
|
rpaths = `otool -l "#{exe}" | grep -A 2 'cmd LC_RPATH' | grep 'path'`
|
|
|
|
unless rpaths.include?(rpath)
|
|
while_writable(exe) do
|
|
system('install_name_tool', '-add_rpath',
|
|
File.join('@executable_path', rel_path), exe)
|
|
end
|
|
end
|
|
|
|
`otool -L "#{exe}"`.split("\n")[1..-1].each do |line|
|
|
match = line.match(%r{^\s+(.+/(lib[^/ ]+))\s})
|
|
next unless match && match[1].start_with?(lib_source)
|
|
|
|
while_writable(exe) do
|
|
if match[2] == exe_file
|
|
system('install_name_tool', '-id',
|
|
File.join('@executable_path', rel_path, match[2].to_s), exe)
|
|
else
|
|
system('install_name_tool', '-change', match[1],
|
|
File.join('@executable_path', rel_path, match[2].to_s), exe)
|
|
end
|
|
end
|
|
|
|
next if match[2] == exe_file || File.exist?(File.join(lib_dir, match[2]))
|
|
|
|
FileUtils.mkdir_p(lib_dir)
|
|
FileUtils.cp(match[1], lib_dir)
|
|
copy_libs(File.join(lib_dir, match[2].to_s), rel_path)
|
|
end
|
|
end
|
|
|
|
def copy_extra_libs(extra_libs, exe, rel_path = nil)
|
|
rel_path ||= Pathname.new(lib_dir).relative_path_from(
|
|
Pathname.new(File.dirname(exe))
|
|
).to_s
|
|
|
|
extra_libs.each do |lib|
|
|
lib_file = File.basename(lib)
|
|
target = "#{lib_dir}/#{lib_file}"
|
|
unless File.exist?(target)
|
|
FileUtils.mkdir_p(lib_dir)
|
|
FileUtils.cp(lib, lib_dir)
|
|
end
|
|
|
|
while_writable(target) do
|
|
system('install_name_tool', '-id',
|
|
File.join('@executable_path', rel_path, lib_file), target)
|
|
end
|
|
|
|
copy_libs(target, rel_path)
|
|
end
|
|
end
|
|
|
|
def while_writable(file)
|
|
mode = File.stat(file).mode
|
|
File.chmod(0o775, file)
|
|
yield
|
|
ensure
|
|
File.chmod(mode, file)
|
|
end
|
|
end
|
|
|
|
class GccLibEmbedder < AbstractEmbedder
|
|
attr_reader :gcc_info
|
|
|
|
def initialize(app, gcc_info)
|
|
super(app)
|
|
@gcc_info = gcc_info
|
|
end
|
|
|
|
def embed
|
|
if embedded?
|
|
info 'libgccjit already embedded in Emacs.app'
|
|
return
|
|
end
|
|
|
|
info 'Embedding libgccjit into Emacs.app'
|
|
|
|
if gcc_info.lib_dir.empty?
|
|
err "No suitable GCC lib dir found in #{gcc_info.root_dir}"
|
|
end
|
|
|
|
FileUtils.mkdir_p(File.dirname(target_dir))
|
|
FileUtils.cp_r(source_dir, target_dir)
|
|
FileUtils.rm(Dir[File.join(target_dir, '**', '.DS_Store')], force: true)
|
|
FileUtils.chmod_R('u+w', target_dir)
|
|
FileUtils.mv(source_darwin_dir, target_darwin_dir)
|
|
end
|
|
|
|
private
|
|
|
|
def embedded?
|
|
Dir[File.join(target_dir, 'libgcc*')].any?
|
|
end
|
|
|
|
def target_dir
|
|
File.join(invocation_dir, gcc_info.relative_lib_dir)
|
|
end
|
|
|
|
def source_darwin_dir
|
|
File.join(invocation_dir, gcc_info.relative_darwin_lib_dir)
|
|
end
|
|
|
|
def target_darwin_dir
|
|
File.join(invocation_dir, gcc_info.sanitized_relative_darwin_lib_dir)
|
|
end
|
|
|
|
def source_dir
|
|
gcc_info.lib_dir
|
|
end
|
|
|
|
def relative_dir(path, root)
|
|
Pathname.new(path).relative_path_from(Pathname.new(root)).to_s
|
|
end
|
|
end
|
|
|
|
class GccInfo
|
|
include Output
|
|
|
|
def root_dir
|
|
@root_dir ||= `brew --prefix gcc`.chomp
|
|
end
|
|
|
|
def major_version
|
|
@major_version ||= File.basename(lib_dir)
|
|
end
|
|
|
|
def lib_dir
|
|
@lib_dir ||= Dir[File.join(root_dir, 'lib/gcc/*/libgcc*')]
|
|
.map { |path| File.dirname(path) }
|
|
.select { |path| File.basename(path).match(/^\d+$/) }
|
|
.max_by { |path| File.basename(path).to_i }
|
|
end
|
|
|
|
def relative_lib_dir
|
|
@relative_lib_dir ||= relative_dir(lib_dir, root_dir)
|
|
end
|
|
|
|
def darwin_lib_dir
|
|
@darwin_lib_dir ||= Dir[
|
|
File.join(lib_dir, 'gcc/*apple-darwin*/*')
|
|
].max_by do |path|
|
|
[
|
|
File.basename(File.dirname(path)).match(/darwin(\d+)$/)[1].to_i,
|
|
File.basename(path).split('.').map(&:to_i)
|
|
]
|
|
end
|
|
end
|
|
|
|
def relative_darwin_lib_dir
|
|
@relative_darwin_lib_dir ||= relative_dir(darwin_lib_dir, root_dir)
|
|
end
|
|
|
|
# Sanitize folder name with full "MAJOR.MINOR.PATCH" version number to just
|
|
# the MAJOR version. Apple's codesign CLI tool throws a "bundle format
|
|
# unrecognized" error if there are any folders with two dots in their name
|
|
# within the Emacs.app application bundle.
|
|
def sanitized_relative_darwin_lib_dir
|
|
@sanitized_relative_darwin_lib_dir ||= File.join(
|
|
File.dirname(relative_darwin_lib_dir),
|
|
File.basename(relative_darwin_lib_dir).split('.').first
|
|
)
|
|
end
|
|
|
|
def libgccjit_root_dir
|
|
@libgccjit_root_dir ||= `brew --prefix libgccjit`.chomp
|
|
end
|
|
|
|
def libgccjit_major_version
|
|
@libgccjit_major_version ||= File.basename(libgccjit_lib_dir.to_s)
|
|
end
|
|
|
|
def libgccjit_lib_dir
|
|
@libgccjit_lib_dir ||= Dir[
|
|
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 }
|
|
end
|
|
|
|
def verify_libgccjit
|
|
err 'gcc not installed' unless Dir.exist?(root_dir)
|
|
err 'libgccjit not installed' unless Dir.exist?(libgccjit_root_dir)
|
|
|
|
if libgccjit_lib_dir&.empty?
|
|
err "Detected libgccjit (#{libgccjit_root_dir}) does not have any " \
|
|
'libgccjit.so* files. Please try reinstalling libgccjit: ' \
|
|
'brew reinstall libgccjit'
|
|
end
|
|
|
|
return if major_version == libgccjit_major_version
|
|
|
|
err <<~TEXT
|
|
Detected GCC and libgccjit library paths do not belong to the same major
|
|
version of GCC. Detected paths:
|
|
- #{lib_dir}
|
|
- #{libgccjit_lib_dir}
|
|
TEXT
|
|
end
|
|
|
|
def get_binding
|
|
binding
|
|
end
|
|
|
|
private
|
|
|
|
def relative_dir(path, root)
|
|
Pathname.new(path).relative_path_from(Pathname.new(root)).to_s
|
|
end
|
|
end
|
|
|
|
if __FILE__ == $PROGRAM_NAME
|
|
cli_options = {
|
|
work_dir: File.expand_path(__dir__),
|
|
native_full_aot: false,
|
|
native_march: false,
|
|
parallel: Etc.nprocessors,
|
|
rsvg: true,
|
|
xwidgets: true,
|
|
github_auth: true,
|
|
dist_include: ['COPYING'],
|
|
archive: true,
|
|
archive_keep: false
|
|
}
|
|
|
|
begin
|
|
OptionParser.new do |opts|
|
|
opts.banner = <<~DOC
|
|
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:
|
|
DOC
|
|
|
|
opts.on('-j', '--parallel COUNT',
|
|
'Compile using COUNT parallel processes ' \
|
|
"(detected: #{cli_options[:parallel]})") do |v|
|
|
cli_options[:parallel] = v
|
|
end
|
|
|
|
opts.on('--git-sha SHA',
|
|
'Override detected git SHA of specified ' \
|
|
'branch allowing builds of old commits') do |v|
|
|
cli_options[:git_sha] = v
|
|
end
|
|
|
|
opts.on('--[no-]xwidgets',
|
|
'Enable/disable XWidgets if supported ' \
|
|
'(default: enabled)') do |v|
|
|
cli_options[:xwidgets] = v
|
|
end
|
|
|
|
opts.on('--[no-]native-comp',
|
|
'Enable/disable native-comp ' \
|
|
'(default: enabled if supported)') do |v|
|
|
cli_options[:native_comp] = v
|
|
end
|
|
|
|
opts.on('--[no-]native-march',
|
|
'Enable/disable -march=native CFLAG' \
|
|
'(default: disabled)') do |v|
|
|
cli_options[:native_march] = v
|
|
end
|
|
|
|
opts.on('--[no-]native-full-aot',
|
|
'Enable/disable NATIVE_FULL_AOT / Ahead of Time compilation ' \
|
|
'(default: disabled)') do |v|
|
|
cli_options[:native_full_aot] = v
|
|
end
|
|
|
|
opts.on('--[no-]rsvg',
|
|
'Enable/disable SVG image support via librsvg ' \
|
|
'(default: enabled)') do |v|
|
|
cli_options[:rsvg] = v
|
|
end
|
|
|
|
opts.on('--no-titlebar', 'Apply no-titlebar patch (default: disabled)') do
|
|
cli_options[:no_titlebar] = true
|
|
end
|
|
|
|
opts.on('--no-frame-refocus',
|
|
'Apply no-frame-refocus patch (default: disabled)') do
|
|
cli_options[:no_frame_refocus] = true
|
|
end
|
|
|
|
opts.on('--[no-]github-auth',
|
|
'Make authenticated GitHub API requests if GITHUB_TOKEN ' \
|
|
'environment variable is set.' \
|
|
'(default: enabled)') do |v|
|
|
cli_options[:github_auth] = v
|
|
end
|
|
|
|
opts.on('--work-dir DIR',
|
|
'Specify a working directory where tarballs, sources, and ' \
|
|
'builds will be stored and worked with') do |v|
|
|
cli_options[:work_dir] = v
|
|
end
|
|
|
|
opts.on('-o DIR', '--output DIR',
|
|
'Output directory for finished builds ' \
|
|
'(default: <work-dir>/builds)') do |v|
|
|
cli_options[:output] = v
|
|
end
|
|
|
|
opts.on('--build-name NAME', 'Override generated build name') do |v|
|
|
cli_options[:build_name] = v
|
|
end
|
|
|
|
opts.on('--dist-include x,y,z',
|
|
'List of extra files to copy from Emacs source into build ' \
|
|
'folder/archive (default: COPYING)') do |v|
|
|
cli_options[:dist_include] = v
|
|
end
|
|
|
|
opts.on('--[no-]archive',
|
|
'Enable/disable creating *.tbz archive (default: enabled)') do |v|
|
|
cli_options[:archive] = v
|
|
end
|
|
|
|
opts.on('--[no-]archive-keep-build-dir',
|
|
'Enable/disable keeping source folder for archive ' \
|
|
'(default: disabled)') do |v|
|
|
cli_options[:archive_keep] = v
|
|
end
|
|
|
|
opts.on(
|
|
'--plan FILE',
|
|
'Follow given plan file, instead of using given git ref/sha'
|
|
) do |v|
|
|
cli_options[:plan] = v
|
|
end
|
|
end.parse!
|
|
|
|
work_dir = cli_options.delete(:work_dir)
|
|
Build.new(work_dir, ARGV.shift, cli_options).build
|
|
rescue Error => e
|
|
warn "ERROR: #{e.message}"
|
|
exit 1
|
|
end
|
|
end
|