-
-
Save burke/9094b0e5206f8c61c737db479b76b4ee to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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