Compare commits

..

10 Commits

Author SHA1 Message Date
8d165b627b wip: add commit-info command 2021-04-26 00:23:14 +01:00
9c977a75b5 wip: extract tarball downloading into separate command 2021-04-26 00:12:07 +01:00
2054c8c0aa chore(release): 0.4.10 2021-04-26 00:07:30 +01:00
ca09d1a95f docs(readme): update readme about native-comp being merged to master 2021-04-26 00:07:07 +01:00
f1e60e31d9 chore(makefile): add next-version target to preview new-version 2021-04-25 23:09:18 +01:00
8d197aea73 chore: ensure file mode is restored in case of error 2021-04-25 23:07:54 +01:00
844df73c8f fix(cli): correctly default to master branch if no git ref is given
Previously it would build master, but not include "master" in the
final output archive's file name.
2021-04-25 23:04:03 +01:00
f1fc68c8f5 chore(release): 0.4.9 2021-04-08 12:03:41 +01:00
e19a6a7bc2 fix(cli): default to "master" if no git ref is given
Fixes #35
2021-04-08 12:02:45 +01:00
1000999eb2 fix(native_comp): skip symlink creation for recent builds which do not need symlinks
Recent builds places the native-lisp cache folder within
Contents/Resources on macOS, and correctly deals with finding them. This
means the Contents/lisp and Contents/native-lisp symlinks are no longer
needed.

Hence we skip their creation altogether if we find any
Contents/Resources/native-lisp/**/*.eln files.
2021-04-08 11:54:01 +01:00
15 changed files with 440 additions and 30 deletions

View File

@@ -2,6 +2,21 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
### [0.4.10](https://github.com/jimeh/build-emacs-for-macos/compare/0.4.9...0.4.10) (2021-04-25)
### Bug Fixes
* **cli:** correctly default to master branch if no git ref is given ([844df73](https://github.com/jimeh/build-emacs-for-macos/commit/844df73c8fa8440e657f7900ec89cdedb7c4c312))
### [0.4.9](https://github.com/jimeh/build-emacs-for-macos/compare/0.4.8...0.4.9) (2021-04-08)
### Bug Fixes
* **cli:** default to "master" if no git ref is given ([e19a6a7](https://github.com/jimeh/build-emacs-for-macos/commit/e19a6a7bc24379292ee06ae4c805b8c5365f2d97)), closes [#35](https://github.com/jimeh/build-emacs-for-macos/issues/35)
* **native_comp:** skip symlink creation for recent builds which do not need symlinks ([1000999](https://github.com/jimeh/build-emacs-for-macos/commit/1000999eb2673dc207a390ff3f902b9987b99173))
### [0.4.8](https://github.com/jimeh/build-emacs-for-macos/compare/0.4.7...0.4.8) (2021-02-27)

View File

@@ -1,9 +1,12 @@
.PHONY: new-version
new-version:
$(if $(shell which npx),,\
$(error No npx found in PATH, please install NodeJS))
$(if $(shell which standard-version),,\
$(error No standard-version found in PATH, install with: \
npm install -g standard-version))
new-version: check-npx
npx standard-version
.PHONY: next-version
next-version: check-npx
npx standard-version --dry-run
.PHONY: check-npx
check-npx:
$(if $(shell which npx),,\
$(error No npx execuable found in PATH, please install NodeJS))

View File

@@ -1,7 +1,8 @@
# 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.
Emacs.app application on macOS, from any git branch, tag, or ref. With support
for native-compilation.
Use this script at your own risk.
@@ -18,7 +19,7 @@ Use this script at your own risk.
## Status
As of writing (2021-01-15) it works for me on my machine. Your luck may vary.
As of writing (2021-04-25) it works for me on my machine. Your luck may vary.
I have successfully built:
@@ -29,8 +30,8 @@ I have successfully built:
For reference, my machine is:
- 13-inch MacBook Pro (2020), 10th-gen 2.3 GHz Quad-Core Intel Core i7 (4c/8t)
- macOS Big Sur 11.1 (20C69)
- Xcode 12.3 (12C33)
- macOS Big Sur 11.2.3 (20D91)
- Xcode 12.4 (12D4e)
## Limitations
@@ -92,8 +93,8 @@ trash the corresponding directory from the `sources` directory.
### Examples
To download a tarball of the `master` branch (Emacs 28.x as of writing) and
build Emacs.app from it:
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
@@ -136,24 +137,24 @@ tools seems to use it to figure out the path to Emacs' executable, including
## Native-Comp
Building a Emacs.app with native-comp support
([gccemacs](https://akrl.sdf.org/gccemacs.html)) from the `feature/native-comp`
branch is now supported without much hassle thanks to the newly released
`libgccjit` Homebrew formula.
_Note: On 2021-04-25 the `feature/native-comp` branch was
[merged](http://git.savannah.gnu.org/cgit/emacs.git/commit/?id=289000eee729689b0cf362a21baa40ac7f9506f6)
into `master`._
To build a Emacs.app with native compilation enabled, simply run:
```
./build-emacs-for-macos feature/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 this
to force it on/off by passing `--native-comp` or `--no-native-comp`
respectfully.
By default `NATIVE_FULL_AOT` is disabled which ensures a fast build by native
compiling as few lisp source files as possible to build the app. Any remaining
lisp files will be dynamically compiled in the background the first time you use
them. To enable native full AoT, pass in the `--native-full-aot` option.
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.
On my machine it takes around 10 minutes to build Emacs.app with
`NATIVE_FULL_AOT` disabled. With it enabled it takes around 20-25 minutes.
To enable native full Ahead-of-Time compilation, pass in the `--native-full-aot`
option, which will native-compile all of Emacs' elisp as 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

52
bin/commit-info Executable file
View File

@@ -0,0 +1,52 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'optparse'
require_relative '../lib/commit_info'
require_relative '../lib/errors'
require_relative '../lib/log'
options = {
repo: 'emacs-mirror/emacs',
output: File.expand_path('../tarballs', __dir__),
log_level: :info
}
OptionParser.new do |opts|
opts.banner = <<~TXT
Usage: ./commit-info [options] <branch/tag/sha>
Fetch commit info of given GitHub repository branch, tag, or SHA.
Options:
TXT
opts.on('-r', '--repo STRING',
"GitHub repository (default: #{options[:repo]})") do |v|
options[:repo] = v
end
opts.on('-o', '--output DIR', 'Directory to save tarball in ' \
"(default: #{options[:output]})") do |v|
options[:output] = v
end
opts.on('-l', '--log-level LEVEL', 'Log level ' \
"(default: #{options[:log_level]})") do |v|
options[:log_level] = v.to_sym
end
end.parse!
begin
logger = Log.new('commit-info', options[:log_level])
commit = CommitInfo.new(
ref: ARGV[0],
repo: options[:repo],
logger: logger
).perform
puts commit.to_yaml
rescue Error => e
handle_error(e)
end

53
bin/download-tarball Executable file
View File

@@ -0,0 +1,53 @@
#!/usr/bin/env ruby
# frozen_string_literal: true
require 'optparse'
require_relative '../lib/download_tarball'
require_relative '../lib/errors'
require_relative '../lib/log'
options = {
repo: 'emacs-mirror/emacs',
output: File.expand_path('../tarballs', __dir__),
log_level: :info
}
OptionParser.new do |opts|
opts.banner = <<~TXT
Usage: ./download-tarball [options] <branch/tag/sha>
Download a tarball of given GitHub repository branch, tag, or SHA.
Options:
TXT
opts.on('-r', '--repo STRING',
"GitHub repository (default: #{options[:repo]})") do |v|
options[:repo] = v
end
opts.on('-o', '--output DIR', 'Directory to save tarball in ' \
"(default: #{options[:output]})") do |v|
options[:output] = v
end
opts.on('-l', '--log-level LEVEL', 'Log level ' \
"(default: #{options[:log_level]})") do |v|
options[:log_level] = v.to_sym
end
end.parse!
begin
logger = Log.new('download-tarball', options[:log_level])
tarball = DownloadTarball.new(
ref: ARGV[0],
repo: options[:repo],
output: options[:output],
logger: logger
).perform
puts tarball.to_yaml
rescue Error => e
handle_error(e)
end

View File

@@ -81,7 +81,7 @@ class Build
def initialize(root_dir, ref = nil, options = {})
@root_dir = root_dir
@ref = ref
@ref = ref || 'master'
@options = options
@gcc_info = GccInfo.new
end
@@ -339,13 +339,20 @@ class Build
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
# 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
@@ -396,6 +403,7 @@ class Build
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?
@@ -669,6 +677,7 @@ class LibEmbedder < AbstractEmbedder
mode = File.stat(file).mode
File.chmod(0o775, file)
yield
ensure
File.chmod(mode, file)
end
end

9
lib/base_action.rb Normal file
View File

@@ -0,0 +1,9 @@
# frozen_string_literal: true
require_relative './common'
require_relative './output'
class BaseAction
include Common
include Output
end

46
lib/commit.rb Normal file
View File

@@ -0,0 +1,46 @@
# frozen_string_literal: true
require 'json'
require 'time'
require 'yaml'
require_relative './errors'
require_relative './common'
class Commit
include Common
attr_reader :repo
attr_reader :ref
attr_reader :message
attr_reader :sha
attr_reader :time
def initialize(sha:, time:, repo: nil, ref: nil, message: nil)
@sha = sha
@time = time
@repo = repo
@ref = ref
@message = message
end
def sha_short
sha[0..6]
end
def to_hash
{
'repo' => repo,
'ref' => ref,
'sha' => sha,
'sha_short' => sha_short,
'time' => time.utc,
'timestamp' => time.utc.to_i,
'message' => message
}
end
def to_yaml
to_hash.to_yaml
end
end

43
lib/commit_info.rb Normal file
View File

@@ -0,0 +1,43 @@
# frozen_string_literal: true
require_relative './base_action'
require_relative './commit'
class CommitInfo < BaseAction
COMMIT_URL = 'https://api.github.com/repos/%s/commits/%s'
attr_reader :ref
attr_reader :repo
attr_reader :logger
def initialize(ref:, repo:, logger:)
@ref = ref
@repo = repo
@logger = logger
err 'branch/tag/sha argument cannot be empty' if ref.nil? || ref.empty?
end
def perform
info "Fetching info for git ref: #{ref}"
url = format(COMMIT_URL, repo, ref)
commit_json = http_get(url)
err "Failed to get commit info about: #{ref}" if commit_json.nil?
parsed = JSON.parse(commit_json)
commit = Commit.new(
repo: repo,
ref: ref,
sha: parsed&.dig('sha'),
message: parsed&.dig('commit', 'message'),
time: Time.parse(parsed&.dig('commit', 'committer', 'date')).utc
)
err 'Failed to get commit SHA' if commit.sha.nil? || commit.sha.empty?
err 'Failed to get commit time' if commit.time.nil?
commit
end
end

23
lib/common.rb Normal file
View File

@@ -0,0 +1,23 @@
# frozen_string_literal: true
require 'net/http'
module Common
private
def self.included(base)
base.extend(self)
end
def run_cmd(*args)
info "executing: #{args.join(' ')}"
system(*args) || err("Exit code: #{$CHILD_STATUS.exitstatus}")
end
def http_get(url)
response = Net::HTTP.get_response(URI.parse(url))
return unless response.code == '200'
response.body
end
end

61
lib/download_tarball.rb Normal file
View File

@@ -0,0 +1,61 @@
# frozen_string_literal: true
require 'fileutils'
require 'json'
require 'time'
require_relative './base_action'
require_relative './commit_info'
require_relative './tarball'
class DownloadTarball < BaseAction
TARBALL_URL = 'https://github.com/%s/tarball/%s'
attr_reader :ref
attr_reader :repo
attr_reader :output
attr_reader :logger
def initialize(ref:, repo:, output:, logger:)
@ref = ref
@repo = repo
@output = output
@logger = logger
err 'branch/tag/sha argument cannot be empty' if ref.nil? || ref.empty?
end
def perform
FileUtils.mkdir_p(output)
tarball = Tarball.new(file: target, commit: commit)
if File.exist?(target)
info "#{filename} already exists locally, attempting to use."
return tarball
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.' if !result || !File.exist?(target)
tarball
end
def url
@url ||= format(TARBALL_URL, repo, commit.sha)
end
def filename
@filename ||= "#{repo.gsub(/[^\w]/, '-')}-#{commit.sha_short}.tgz"
end
def target
@target ||= File.join(output, filename)
end
def commit
@commit ||= CommitInfo.new(ref: ref, repo: repo, logger: logger).perform
end
end

12
lib/errors.rb Normal file
View File

@@ -0,0 +1,12 @@
# frozen_string_literal: true
def handle_error(err)
warn "ERROR: #{err.message}"
Process.exit 1
end
class Error < StandardError; end
class CommitNotFound < Error; end
class NoCommitSHA < Error; end
class NoCommitTime < Error; end

31
lib/log.rb Normal file
View File

@@ -0,0 +1,31 @@
# frozen_string_literal: true
class Log
extend Forwardable
attr_reader :name
attr_reader :level
def initialize(name, level = :info)
@name = name
@level = level
end
def_delegators :logger, :debug, :info, :warn, :error, :fatal, :unkonwn
private
def logger
@logger ||= Logger.new($stderr).tap do |l|
l.progname = name
l.level = level
l.formatter = formatter
end
end
def formatter
proc do |severity, _datetime, progname, msg|
"==> [#{progname}] #{severity}: #{msg}\n"
end
end
end

28
lib/output.rb Normal file
View File

@@ -0,0 +1,28 @@
# frozen_string_literal: true
require 'forwardable'
require 'logger'
require_relative './errors'
module Output
extend Forwardable
def self.included(base)
base.extend(ClassMethods)
end
module ClassMethods
def logger_name(name = nil)
return @logger_name if name.nil?
@logger_name = name
end
end
def_delegators :logger, :debug, :info, :warn, :error, :fatal, :unkonwn
def err(msg = nil)
raise Error, msg
end
end

24
lib/tarball.rb Normal file
View File

@@ -0,0 +1,24 @@
# frozen_string_literal: true
require 'yaml'
class Tarball
attr_reader :file
attr_reader :commit
def initialize(file:, commit:)
@file = file
@commit = commit
end
def to_hash
{
'file' => file,
'commit' => commit.to_hash
}
end
def to_yaml
to_hash.to_yaml
end
end