mirror of
https://github.com/jimeh/build-emacs-for-macos.git
synced 2026-02-19 13:06:38 +00:00
890 lines
23 KiB
Ruby
Executable File
890 lines
23 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 'uri'
|
|
|
|
class Error < StandardError; end
|
|
|
|
module Output
|
|
def info(msg, newline: true)
|
|
out "INFO: #{msg}", newline: newline
|
|
end
|
|
|
|
def out(msg, newline: true)
|
|
if newline
|
|
puts "==> #{msg}"
|
|
else
|
|
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
|
|
@options = options
|
|
@gcc_info = GccInfo.new
|
|
end
|
|
|
|
def build
|
|
unless meta[:sha] && meta[:date]
|
|
err 'Failed to get commit info from GitHub API.'
|
|
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 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' unless options[:rsvg]
|
|
|
|
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_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
|
|
|
|
if options[:native_comp]
|
|
make_flags << "BYTE_COMPILE_EXTRA_FLAGS=--eval '(setq comp-speed 2)'"
|
|
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]
|
|
|
|
info 'Creating symlinks within Emacs.app needed for native-comp'
|
|
|
|
FileUtils.cd(File.join(app, 'Contents')) do
|
|
FileUtils.ln_s('Resources/lisp', 'lisp') unless File.exist?('lisp')
|
|
|
|
source = Dir['MacOS/libexec/emacs/**/eln-cache',
|
|
'MacOS/lib/emacs/**/native-lisp'].first
|
|
target = File.basename(source)
|
|
FileUtils.ln_s(source, target) unless File.exist?(target)
|
|
end
|
|
end
|
|
|
|
def archive_app(app)
|
|
FileUtils.mkdir_p(builds_dir)
|
|
|
|
metadata = [
|
|
meta[:ref]&.gsub(/\W/, '-'),
|
|
meta[:date],
|
|
meta[:sha][0..6],
|
|
"macOS-#{OS.version}",
|
|
OS.arch
|
|
].compact
|
|
|
|
filename = "Emacs.app-[#{metadata.join('][')}].tbz"
|
|
target = "#{builds_dir}/#{filename}"
|
|
|
|
app_base = File.basename(app)
|
|
app_dir = File.dirname(app)
|
|
|
|
if !File.exist?(target)
|
|
info "Creating #{filename} archive in \"#{builds_dir}\"..."
|
|
FileUtils.cd(app_dir) { system('tar', '-cjf', target, app_base) }
|
|
else
|
|
info "#{filename} archive exists in " \
|
|
"#{builds_dir.gsub(root_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
|
|
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: Date.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?
|
|
self_ref_libs(binary)
|
|
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
|
|
|
|
`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 self_ref_libs(exe)
|
|
rel_path = Pathname.new(lib_dir).relative_path_from(
|
|
Pathname.new(File.dirname(exe))
|
|
).to_s
|
|
lib_paths ||= Dir.glob("#{lib_dir}/*").select { |f| File.file?(f) }
|
|
libs = lib_paths.map { |f| File.basename(f) }
|
|
|
|
([exe] + lib_paths).each do |bin_path|
|
|
`otool -L "#{bin_path}"`.split("\n")[1..-1].each do |line|
|
|
match = line.match(%r{^\s+(.+/(lib[^/ ]+))\s})
|
|
next unless match
|
|
next if match[1].start_with?('@executable_path/')
|
|
next unless libs.include?(match[2])
|
|
|
|
while_writable(bin_path) do
|
|
system(
|
|
'install_name_tool', '-change', match[1],
|
|
File.join('@executable_path', rel_path, match[2].to_s),
|
|
bin_path
|
|
)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
def while_writable(file)
|
|
mode = File.stat(file).mode
|
|
File.chmod(0o775, file)
|
|
yield
|
|
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)
|
|
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_dir
|
|
gcc_info.lib_dir
|
|
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
|
|
|
|
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)
|
|
root += '/' unless root[-1] == '/'
|
|
return if path[0..root.size - 1] != root
|
|
|
|
path[root.size..-1]
|
|
end
|
|
end
|
|
|
|
if __FILE__ == $PROGRAM_NAME
|
|
cli_options = {
|
|
native_full_aot: false,
|
|
parallel: Etc.nprocessors,
|
|
rsvg: false,
|
|
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 ' \
|
|
"(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 ' \
|
|
'(default: enabled if supported)') 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('--rsvg', 'Enable SVG image support via librsvg, ' \
|
|
'can yield a unstable build (default: disabled)') do
|
|
cli_options[:rsvg] = true
|
|
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-]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!
|
|
|
|
Build.new(File.expand_path(__dir__), ARGV.shift, cli_options).build
|
|
rescue Error => e
|
|
warn "ERROR: #{e.message}"
|
|
exit 1
|
|
end
|
|
end
|