Skip to content

Instantly share code, notes, and snippets.

@burke
Created May 26, 2020 17:07
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save burke/9094b0e5206f8c61c737db479b76b4ee to your computer and use it in GitHub Desktop.
Save burke/9094b0e5206f8c61c737db479b76b4ee to your computer and use it in GitHub Desktop.
#!/usr/bin/ruby --disable-gems
require('open3')
module ShellSupportCLI
ROOT = File.expand_path('../..', __FILE__)
NIX_WAIT_MSG = "Waiting for Nix to mount..."
NIX_TIMEOUT_MESSAGE = <<-EOM
\e[31m\e[01m
\u{1f469}\u{200d}\u{1f4bb} Nix did not mount in time. Please open another terminal
\u{1f469}\u{200d}\u{1f4bb} and try again. If you see this message a second time,
\u{1f469}\u{200d}\u{1f4bb} `dev up` in any project to re-initialize Nix. If that still
\u{1f469}\u{200d}\u{1f4bb} fails, you can try rebooting your computer, as
\u{1f469}\u{200d}\u{1f4bb} this has worked for some. If you need to do this,
\u{1f469}\u{200d}\u{1f4bb} please let us know in #dev-infrastructure as this
\u{1f469}\u{200d}\u{1f4bb} is a bug we'd like to squash.
\e[0m
EOM
class << self
def call(*argv)
case argv.shift
when 'env'
usage_and_die unless (shellname = argv.shift)
env(shellname)
else
usage_and_die
end
end
private
# write out (to STDOUT) a list of instructions using a simple API
# implemented in each shell shim. This API consists of a small set of commands:
#
# dev__source <path>
# sources a script from the named path. literally just "source <path>".
#
# dev__append_path <path>
# append the named path to $PATH
#
# dev__prepend_path <path>
# prepend the named path to $PATH
#
# dev__setenv <k> <v>
# set the environment variable, e.g. export <k>=<v>
#
# dev__lazyload <command> <path>
# If <command> is not already defined, create a shell function which
# loads the actual implementation of the command from the named path.
# This is useful for rarely-used commands which must be provided, but
# take some time to load, and so should not be bothered with on shell
# startup. (e.g. nvm, and to a lesser degree, chruby)
def env(shellname)
EnvHelpers.source("sh/autocomplete/completion.#{shellname}")
EnvHelpers.setenv('USING_DEV', '1')
cargo_bin = File.expand_path('~/.cargo/bin')
if Dir.exist?(cargo_bin)
EnvHelpers.append_path(cargo_bin)
end
EnvHelpers.lazyload('chruby', "'#{ROOT}/sh/chruby/chruby.sh'")
EnvHelpers.lazyload("nvm", "'#{ROOT}/sh/nvm/nvm.sh' --no-use")
if File.symlink?('/usr/local/bin/pyenv')
pyenv_shims = File.expand_path('~/.pyenv/shims')
EnvHelpers.prepend_path(pyenv_shims)
EnvHelpers.source("sh/pyenv/init.#{shellname}")
end
EnvHelpers.prepend_path("/nix/var/nix/gcroots/dev-profiles/user-extra-profile/bin")
# Normally to load nix, one would source ~/.nix-profile/etc/profile.d/nix.sh.
# However, this script is not actually idempotent, and does a bunch of
# one-time setup things.
#
# Our strategy is to run the script only when necessary for the setup
# elements, and to manually make the same environment changes
# (idempotently...) from here.
if nix_should_already_exist?
EnvHelpers.prepend_path(File.expand_path('~/.nix-profile/bin'))
EnvHelpers.setenv('NIX_SSL_CERT_FILE', File.expand_path('~/.nix-profile/etc/ssl/certs/ca-bundle.crt'))
EnvHelpers.setenv('NIX_PATH', build_nix_path)
end
# That is what we want: A trailing colon indicates that we should fall
# back on the built-in default paths.
unless ENV.fetch('MANPATH', '').include?('.nix-profile/share/man')
EnvHelpers.setenv('MANPATH', "#{File.expand_path('~/.nix-profile/share/man')}:#{ENV['MANPATH']}")
end
run_nix_profile_script_if_necessary
EnvHelpers.prepend_path("#{ROOT}/bin/user")
end
# Make sure each channel in ~/.nix-defexpr/channels is present on the
# NIX_PATH in name=path form, and not just as a blanket
# ~/.nix-defexpr/channels. However, don't touch any other configuration that
# got there before us.
#
# In the end this will probably typically just contain one entry:
# NIX_PATH=nixpkgs=/Users/me/.nix-defexpr/channels/nixpkgs
def build_nix_path
channels_path = File.expand_path('~/.nix-defexpr/channels')
wait_for_nix_store
nix_path = ENV.fetch('NIX_PATH', '').split(':')
channels = Dir.entries(channels_path) - %w(. .. manifest.nix)
channels.each do |channel|
next if nix_path.detect { |e| e.start_with?("#{channel}=") }
path = File.expand_path(channel, channels_path)
nix_path.unshift("#{channel}=#{path}")
end
nix_path.reject! { |f| f == channels_path }
nix_path.join(':')
end
def nix_should_already_exist?
return true if File.exist?('/nix/store')
File.exist?("/etc/fstab") &&
File.read("/etc/fstab").include?("/nix") &&
File.exist?("/etc/synthetic.conf") &&
File.read("/etc/synthetic.conf").include?("nix")
end
def wait_for_nix_store(timeout: 3.0, interval: 0.05)
start_time = Time.now
first = true
while (Time.now - start_time).to_f <= timeout
return if File.exist?('/nix/store')
if first
STDERR.puts(NIX_WAIT_MSG)
first = false
end
sleep(interval)
end
if try_mitigate_nix_missing
sleep(0.1)
return if File.exist?('/nix/store')
end
abort(NIX_TIMEOUT_MESSAGE)
end
def try_mitigate_nix_missing
out, err, stat = Open3.capture3('diskutil', 'info', 'Nix')
return false unless stat.success?
if out.match?(/Locked:\s+Yes/)
uuid = out.scan(/Volume UUID:\s+(\S+)/).flatten.first
STDERR.puts(<<~EOF)
For some reason or another, macOS didn't unlock the Nix volume.
We're going to do that for you, but you'll have to enter your
username and password to retrieve the passphrase from the System
Keychain.
If this keeps happening every time you reboot after updating to macOS
10.15.4, you can run the following script to nuke the entire Nix
volume and rebuild it, which corrects the underlying issue:
sudo diskutil unmount force Nix
sudo diskutil apfs deleteVolume Nix
for i in $(seq 1 20); do sudo security delete-generic-password -l Nix 2>/dev/null; done
export PATH=~/.nix-profile/bin:$PATH
/opt/dev/bin/dev update --no-pull
Press Enter to continue.
EOF
STDIN.gets
out, err, stat = Open3.capture3('security', 'find-generic-password', '-l', 'Nix', '-a', uuid, '-w')
abort("failed to retrieve passphrase: #{err}") unless stat.success?
passphrase = out.chomp
_, err, stat = Open3.capture3('diskutil', 'apfs', 'unlockVolume', 'Nix', '-passphrase', passphrase)
abort("failed to unlock volume: #{err}") unless stat.success?
end
_, err, stat = Open3.capture3('diskutil', 'mount', 'Nix')
abort("failed to mount volume: #{err}") unless stat.success?
true
end
def run_nix_profile_script_if_necessary
if nix_profile_script_necessary?
# Running this not for the variable assignments, which we do ourselves
# above, but only for the side effects.
system(
'/bin/sh',
File.expand_path('~/.nix-profile/etc/profile.d/nix.sh'),
out: '/dev/null', err: '/dev/null',
)
end
end
def nix_profile_script_necessary?
# these are the main conditions that nix.sh will test and attempt to force
# into convergence. We could just execute the script unconditionally, but
# let's save the user the 80ms or whatever it is.
return true unless Dir.exist?("/nix/var/nix/profiles/per-user/#{ENV['USER']}")
return true unless File.symlink?(File.expand_path('~/.nix-profile'))
return true unless File.exist?(File.expand_path('~/.nix-channels'))
return true unless Dir.exist?("/nix/var/nix/gcroots/per-user/#{ENV['USER']}")
false
end
def usage_and_die
abort("usage: #{$PROGRAM_NAME} env <shellname>")
end
end
module EnvHelpers
class << self
def setenv(k, v)
puts %(dev__setenv "#{k}" "#{v}")
end
# bass mode is not supported by bash/zsh; it's just to tell fish to use
# bass to load a posix script.
def source(rel_path, bass: false)
abs_path = File.expand_path(rel_path, ROOT)
if bass
puts %(dev__source_bass "#{abs_path}")
else
puts %(dev__source "#{abs_path}")
end
end
def append_path(path)
puts %(dev__append_path "#{path}") unless ENV['PATH'].include?(path)
end
def prepend_path(path)
puts %(dev__prepend_path "#{path}")
end
def lazyload(cmd, script)
puts %(dev__lazyload "#{cmd}" "#{script}")
end
end
end
end
ShellSupportCLI.call(*ARGV) if $PROGRAM_NAME == __FILE__
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment