mirror of
https://github.com/jimeh/build-emacs-for-macos.git
synced 2026-02-19 13:06:38 +00:00
Apple's codesign CLI toolthrows a "bundle format unrecognized" error if there are any folders within the application that contain two dots in their name. Hence we need to get rid of the one instance of that we end up with from GCC, and update the native-comp patch accordingly. As of writing, this means renaming: Emacs.app/Contents/MacOS/lib/gcc/11/gcc/x86_64-apple-darwin20/11.1.0 To: Emacs.app/Contents/MacOS/lib/gcc/11/gcc/x86_64-apple-darwin20/11
947 lines
25 KiB
Ruby
Executable File
947 lines
25 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
|
|
|
|
DOWNLOAD_URL = 'https://github.com/emacs-mirror/emacs/tarball/%s'
|
|
LATEST_URL = 'https://api.github.com/repos/emacs-mirror/emacs/commits/%s'
|
|
NATIVE_COMP_REF_REGEXP = %r{^feature/native-comp}.freeze
|
|
|
|
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)
|
|
symlink_internals(app)
|
|
|
|
LibEmbedder.new(app, brew_dir, extra_libs).embed
|
|
GccLibEmbedder.new(app, gcc_info).embed if options[:native_comp]
|
|
|
|
archive_app(app)
|
|
end
|
|
|
|
private
|
|
|
|
def load_plan(filename)
|
|
plan = YAML.safe_load(File.read(filename), [:Time])
|
|
|
|
@meta = {
|
|
sha: plan.dig('commit', 'sha'),
|
|
ref: plan.dig('commit', 'ref'),
|
|
date: plan.dig('commit', 'date')
|
|
}
|
|
@archive_filename = plan['archive']
|
|
end
|
|
|
|
def tarballs_dir
|
|
@tarballs_dir ||= File.join(root_dir, 'tarballs')
|
|
end
|
|
|
|
def sources_dir
|
|
@sources_dir ||= File.join(root_dir, 'sources')
|
|
end
|
|
|
|
def builds_dir
|
|
@builds_dir ||= 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.'
|
|
result = run_cmd('curl', '-L', url, '-o', target)
|
|
err 'Download failed.' unless result
|
|
|
|
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',
|
|
'-march=native',
|
|
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 symlink_internals(app)
|
|
return unless options[:native_comp]
|
|
|
|
FileUtils.cd(File.join(app, 'Contents')) 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
|
|
err 'Failed to find native-lisp cache directory for symlink creation.'
|
|
|
|
target = File.basename(source)
|
|
FileUtils.ln_s(source, target) unless File.exist?(target)
|
|
end
|
|
end
|
|
|
|
def archive_filename
|
|
return @archive_filename if @archive_filename
|
|
|
|
metadata = [
|
|
meta[:ref]&.gsub(/\W/, '-'),
|
|
meta[:date]&.strftime('%Y-%m-%d'),
|
|
meta[:sha][0..6],
|
|
"macOS-#{OS.version}",
|
|
OS.arch
|
|
].compact
|
|
|
|
filename = "Emacs.app-[#{metadata.join('][')}].tbz"
|
|
@archive_filename = File.join(builds_dir, filename)
|
|
end
|
|
|
|
def archive_app(app)
|
|
filename = File.basename(archive_filename)
|
|
target_dir = File.dirname(archive_filename)
|
|
relative_target_dir = target_dir.gsub(root_dir + '/', '')
|
|
|
|
FileUtils.mkdir_p(target_dir)
|
|
|
|
app_base = File.basename(app)
|
|
app_dir = File.dirname(app)
|
|
|
|
if !File.exist?(archive_filename)
|
|
info "Creating #{filename} archive in \"#{relative_target_dir}\"..."
|
|
FileUtils.cd(app_dir) do
|
|
system('tar', '-cjf', archive_filename, app_base)
|
|
end
|
|
else
|
|
info "#{filename} archive exists in " \
|
|
"#{relative_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}"
|
|
url = format(LATEST_URL, ref_sha)
|
|
commit_json = http_get(url)
|
|
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 http_get(url)
|
|
response = Net::HTTP.get_response(URI.parse(url))
|
|
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?
|
|
end
|
|
end
|
|
|
|
private
|
|
|
|
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,
|
|
parallel: Etc.nprocessors,
|
|
rsvg: true,
|
|
xwidgets: true
|
|
}
|
|
|
|
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 ' \
|
|
"(default: #{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-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('--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(
|
|
'--plan FILE',
|
|
'Follow given plan file, instead of using given git ref/sha'
|
|
) do |v|
|
|
cli_options[:plan] = v
|
|
end
|
|
|
|
opts.on('--[no-]native-fast-boot',
|
|
'DEPRECATED: use --[no-]native-full-aot instead') do |v|
|
|
if v
|
|
raise Error, '--native-fast-boot option is deprecated, ' \
|
|
'use --no-native-full-aot instead'
|
|
else
|
|
raise Error, '--no-native-fast-boot option is deprecated, ' \
|
|
'use --native-full-aot instead'
|
|
end
|
|
end
|
|
|
|
opts.on('--[no-]launcher',
|
|
'DEPRECATED: Launcher script is no longer used.') do |_|
|
|
raise Error, '--[no-]launcher option is deprecated, launcher ' \
|
|
'script is no longer used.'
|
|
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
|