Add auto-migration support

This commit is contained in:
Zhaofeng Li
2023-07-03 18:32:40 -06:00
parent 7391ea1ddb
commit defb864237
14 changed files with 327 additions and 36 deletions
+45
View File
@@ -0,0 +1,45 @@
name: CI
on:
push:
pull_request:
jobs:
check:
runs-on: macos-13
steps:
- uses: actions/checkout@v3
- uses: DeterminateSystems/nix-installer-action@main
- uses: DeterminateSystems/magic-nix-cache-action@main
- name: Test nuke-homebrew-repository
run: |
nix build .#nuke-homebrew-repository.passthru.tests.test-nuke -L
- name: Install some package with Homebrew
run: |
brew install unbound
- name: Add a third-party tap imperatively
run: |
brew tap koekeishiya/formulae
- name: Set up /run for nix-darwin
run: |
printf 'run\tprivate/var/run\n' | sudo tee -a /etc/synthetic.conf
sudo /System/Library/Filesystems/apfs.fs/Contents/Resources/apfs.util -t || true
- name: Migrate existing Homebrew installation (imperative taps)
run: |
sudo rm /etc/bashrc
./ci/activate-example.sh migrate
- name: Check that we can still use the unbound package
run: $(brew --prefix)/sbin/unbound -V
- name: Check that we can still use the tap that we added imperatively
run: brew install koekeishiya/formulae/yabai
#- name: Migrate to fully-declarative tap management
# run: |
# false
+20
View File
@@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
DIR=$(dirname $0)
if [[ "$#" != "1" ]]; then
>&2 echo "Usage: $0 [example]"
exit 1
fi
example="$1"
nix-instantiate --eval --json -E 'builtins.currentSystem' >"${DIR}/system.json"
systemProfile=$(nix build "./${DIR}/..#darwinConfigurations.ci.${example}.system" -L --no-link --print-out-paths)
>&2 echo "Built $systemProfile"
sudo "$systemProfile/activate"
"$systemProfile/activate-user"
# vim: set et ts=2 sw=2:
+25
View File
@@ -0,0 +1,25 @@
# Activates example configurations on CI
{ self, nix-darwin, nixpkgs, ... }:
let
system = builtins.fromJSON (builtins.readFile ./system.json);
pkgs = nixpkgs.legacyPackages.${system};
inherit (pkgs) lib;
makeProfile = example: nix-darwin.lib.darwinSystem {
inherit system pkgs;
modules = [
self.darwinModules.nix-homebrew
(../examples + "/${example}.nix")
{
documentation.enable = false;
services.nix-daemon.enable = true;
nix-homebrew = {
user = lib.mkForce "runner";
};
}
];
};
in {
migrate = makeProfile "migrate";
}
+1
View File
@@ -0,0 +1 @@
migrate
+1
View File
@@ -0,0 +1 @@
"migrate"
+1
View File
@@ -0,0 +1 @@
"aarch64-darwin"
+10
View File
@@ -0,0 +1,10 @@
# Migrate from an existing Homebrew installation
{ pkgs, ... }:
{
nix-homebrew = {
enable = true;
autoMigrate = true;
user = "yourname";
};
}
Generated
+43 -10
View File
@@ -1,18 +1,19 @@
{
"nodes": {
"flake-compat": {
"brew-src": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"lastModified": 1687876440,
"narHash": "sha256-4hoGm5PUYmz091Pqrii4rpfsLsE7/gjIThHVBq6Vquk=",
"owner": "Homebrew",
"repo": "brew",
"rev": "eff45ef570f265e226f14ce91da72d7a6e7d516a",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"owner": "Homebrew",
"ref": "4.0.26",
"repo": "brew",
"type": "github"
}
},
@@ -34,7 +35,38 @@
"type": "github"
}
},
"nix-darwin": {
"inputs": {
"nixpkgs": "nixpkgs"
},
"locked": {
"lastModified": 1688307440,
"narHash": "sha256-7PTjbN+/+b799YN7Tk2SS5Vh8A0L3gBo8hmB7Y0VXug=",
"owner": "LnL7",
"repo": "nix-darwin",
"rev": "b06bab83bdf285ea0ae3c8e145a081eb95959047",
"type": "github"
},
"original": {
"owner": "LnL7",
"repo": "nix-darwin",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1687274257,
"narHash": "sha256-TutzPriQcZ8FghDhEolnHcYU2oHIG5XWF+/SUBNnAOE=",
"path": "/nix/store/22qgs3skscd9bmrxv9xv4q5d4wwm5ppx-source",
"rev": "2c9ecd1f0400076a4d6b2193ad468ff0a7e7fdc5",
"type": "path"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1688049487,
"narHash": "sha256-100g4iaKC9MalDjUW9iN6Jl/OocTDtXdeAj7pEGIRh4=",
@@ -52,9 +84,10 @@
},
"root": {
"inputs": {
"flake-compat": "flake-compat",
"brew-src": "brew-src",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
"nix-darwin": "nix-darwin",
"nixpkgs": "nixpkgs_2"
}
},
"systems": {
+10 -3
View File
@@ -3,19 +3,23 @@
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
nix-darwin.url = "github:LnL7/nix-darwin";
flake-utils.url = "github:numtide/flake-utils";
flake-compat = {
url = "github:edolstra/flake-compat";
brew-src = {
url = "github:Homebrew/brew/4.0.26";
flake = false;
};
};
outputs = { self, nixpkgs, flake-utils, ... }: let
outputs = { self, nixpkgs, nix-darwin, flake-utils, ... } @ inputs: let
# System types to support.
supportedSystems = [ "x86_64-darwin" "aarch64-darwin" ];
in flake-utils.lib.eachSystem supportedSystems (system: let
pkgs = nixpkgs.legacyPackages.${system};
in {
packages = pkgs.callPackage ./pkgs {
inherit inputs;
};
devShell = pkgs.mkShell {
nativeBuildInputs = with pkgs; [
];
@@ -24,5 +28,8 @@
darwinModules = {
nix-homebrew = ./modules;
};
darwinConfigurations = {
ci = import ./ci inputs;
};
};
}
+30 -9
View File
@@ -32,6 +32,8 @@ let
cfg = config.nix-homebrew;
tools = pkgs.callPackage ../pkgs { };
# TODO: Maybe don't provide a default at all
defaultBrew = pkgs.fetchFromGitHub {
owner = "homebrew";
@@ -289,25 +291,34 @@ let
HOMEBREW_CODE="$HOMEBREW_LIBRARY/Homebrew"
if is_occupied "$HOMEBREW_CODE"; then
# Probably an existing installation
error "An existing $HOMEBREW_CODE is in the way"
error "$HOMEBREW_PREFIX seems to contain an existing copy of Homebrew."
warn "An existing $HOMEBREW_CODE is in the way"
warn "$HOMEBREW_PREFIX seems to contain an existing copy of Homebrew."
if [[ -e "$HOMEBREW_PREFIX/.git" ]]; then
# Looks like an Apple Silicon installation
ohai "There are two ways proceed:"
ohai "1. Use the official uninstallation script to remove Homebrew (you will lose all taps and installed packages)"
ohai "2. Delete $HOMEBREW_CODE and clean up other git files later. The existing Homebrew code will be unused."
ohai "Looks like an Apple Silicon installation (Homebrew prefix is the repository)"
HOMEBREW_REPOSITORY="$HOMEBREW_PREFIX"
elif [[ -e "$HOMEBREW_PREFIX/Homebrew/.git" ]]; then
# Looks like an Intel installation
ohai "There are two ways to proceed:"
ohai "1. Use the official uninstallation script to remove Homebrew (you will lose all taps and installed packages)"
ohai "2. Make a backup of $HOMEBREW_LIBRARY/Taps, delete $HOMEBREW_PREFIX/Homebrew, and move the taps back"
ohai "Looks like an Intel installation (Homebrew repository is under the 'Homebrew' subdirectory)"
HOMEBREW_REPOSITORY="$HOMEBREW_PREFIX/Homebrew"
else
# Custom installation?
ohai "Please uninstall Homebrew and try activating again."
exit 1
fi
exit 1
if [[ -z "${toString cfg.autoMigrate}" ]]; then
ohai "There are two ways to proceed:"
ohai "1. Use the official uninstallation script to remove Homebrew (you will lose all taps and installed packages)"
ohai "2. Set nix-homebrew.autoMigrate = true; to allow nix-homebrew to migrate the installation"
ohai "During auto-migration, nix-homebrew will delete the existing installation while keeping installed packages."
exit 1
fi
ohai "Attempting to migrate Homebrew installation..."
${tools.nuke-homebrew-repository} "$HOMEBREW_REPOSITORY"
fi
if [[ ! -e "$HOMEBREW_PREFIX/${nixMarker}" ]]; then
@@ -442,6 +453,16 @@ in {
type = types.bool;
default = true;
};
autoMigrate = lib.mkOption {
description = ''
Whether to allow nix-homebrew to automatically migrate existing Homebrew installations.
When enabled, the activation script will automatically delete
Homebrew repositories while keeping installed packages.
'';
type = types.bool;
default = false;
};
user = lib.mkOption {
description = lib.mdDoc ''
The user owning the Homebrew directories.
+6
View File
@@ -0,0 +1,6 @@
{ callPackage, inputs ? {} }:
{
nuke-homebrew-repository = callPackage ./nuke-homebrew-repository {
inherit inputs;
};
}
+77
View File
@@ -0,0 +1,77 @@
{ lib
, pkgs
, substituteAll
, runtimeShell
, coreutils
, findutils
, gnugrep
, gnused
, gitMinimal
, inputs
, runCommandLocal
}:
let
script = substituteAll {
name = "nuke-homebrew-repository";
src = ./nuke-homebrew-repository.sh.in;
isExecutable = true;
inherit runtimeShell;
path = lib.makeBinPath [ coreutils findutils gnugrep gnused gitMinimal ];
};
brew-src = inputs.brew-src or (throw "The tests can only be run with flakes");
test-nuke = runCommandLocal "test-nuke" {
nativeBuildInputs = [ gitMinimal ];
} ''
must_exist() {
if [[ ! -e "$1" ]]; then
>&2 echo "Error: $1 should have been kept"
exit 1
fi
}
must_not_exist() {
if [[ -e "$1" ]]; then
>&2 echo "Error: $1 should have been deleted"
exit 1
fi
}
# Set up test Homebrew repo
cp -R ${brew-src}/ brew
chmod -R u+w brew
pushd brew >/dev/null
git init -q
git add -f -A
git -c "user.name=CI" -c "user.email=ci@invalid.tld" commit -qm "Test"
popd >/dev/null
# Create a fake tap
mkdir -p brew/Library/Taps/apple/homebrew-apple
touch brew/Library/Taps/apple/homebrew-apple/content
# Create a fake package
mkdir -p brew/Library/Cellar/some-package/1.0.0
touch brew/Library/Cellar/some-package/1.0.0/content
touch brew/bin/some-command
${script} brew
must_not_exist "brew/README.md"
must_not_exist "brew/Library/Homebrew"
must_exist "brew/Library/Taps/apple/homebrew-apple/content"
must_exist "brew/Library/Cellar/some-package/1.0.0/content"
must_exist "brew/bin/some-command"
>>$out echo "Success"
'';
in script // {
passthru.tests = {
inherit test-nuke;
};
}
@@ -0,0 +1,58 @@
#!@runtimeShell@
set -euo pipefail
# Note that we do rely on GNU-ism here
export PATH="@path@"
# Homebrew keeps state (Library/Taps, bin, etc, ...) as ignored
# files in `$HOMEBREW_REPOSITORY`. We want to uninstall Homebrew
# while keeping all state, so we remove all files tracked by git.
if [[ "$#" != "1" ]]; then
>&2 echo "Usage: $0 <homebrew repository>"
>&2 echo "This script tries to nuke all tracked files in a Homebrew repository."
exit 1
fi
pushd "$1" >/dev/null
temp_gitconfig=$(mktemp gitconfig.XXXXXXXXXX)
echo -e "[safe]\ndirectory = $(readlink -f $1)" >"${temp_gitconfig}"
export GIT_CONFIG_GLOBAL="${temp_gitconfig}"
finish() {
rm -f "${temp_gitconfig}"
}
trap finish EXIT
if [[ ! -e ".git" ]]; then
>&2 echo "${PWD} is not a git repository"
exit 1
fi
if ! grep -E "^# Homebrew" "README.md" >/dev/null; then
>&2 echo "${PWD} does not looks like a Homebrew checkout"
exit 1
fi
# First, delete all files
files_to_delete=()
while read -r file; do
if [[ -e "$file" ]] || [[ -L "$file" ]]; then
files_to_delete+=("$file")
fi
done < <(git ls-files)
printf "%s\0" "${files_to_delete[@]}" | xargs -0 rm
# Next, delete all directories
while read -r dir; do
if [[ -d "$dir" ]]; then
rmdir -p --ignore-fail-on-non-empty "$dir"
fi
done < <(git ls-files | sed 's,/[^/]*$,,' | sort | uniq)
# Finally, delete .git
rm -rf --one-file-system ".git"
# vim: set et ts=2 sw=2:
-14
View File
@@ -1,14 +0,0 @@
let
lock = builtins.fromJSON (builtins.readFile ./flake.lock);
flake-compat = builtins.fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/${lock.nodes.flake-compat.locked.rev}.tar.gz";
sha256 = lock.nodes.flake-compat.locked.narHash;
};
flake = import flake-compat {
src = ./.;
};
shell = flake.shellNix.default;
in shell