Last active
September 24, 2019 16:49
-
-
Save duijf/45d0a636396761cf279ad84e1598fa56 to your computer and use it in GitHub Desktop.
Nix installation script without channels and other magic. May eat your hard drive.
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/env python3.7 | |
""" | |
Bootstrap or update a single user Nix installation. | |
Caveat emptor: may eat your hard-drive. Read the script first and check that you | |
like what it does. | |
(C) Laurens Duijvesteijn 2019. Licensed under the MPL 2.0 available from | |
https://www.mozilla.org/en-US/MPL/2.0/ | |
Dependencies: Python3.7, GPG. | |
The code here is an alternative to the single-user Nix installation script. | |
The script downloads and extracts the closure of the Nix package manager to | |
`/nix/store`. It then calls the commands to initialize the Nix database. It ends | |
by printing a config snippet to add to your shell config. | |
The script asks for your sudo password when the `/nix` directory does not exist | |
yet. If it does AND it is owned by your user, the script proceeds as normal. If | |
it is owned by another user, the script aborts. | |
This script should serve as a lightweight Nix installation procedure that relies | |
as little as possible on global state. It offers a subset of the functionality | |
of the regular installer. | |
I personally offer this script as a convenience for my own Nix based | |
repositories and for the systems I manage. | |
""" | |
import dataclasses | |
import grp | |
import os | |
import pathlib | |
import pwd | |
import shutil | |
import subprocess | |
import sys | |
import tarfile | |
import tempfile | |
import urllib.request as request | |
from textwrap import dedent | |
from typing import IO | |
# https://github.com/NixOS/nixos-homepage/blob/master/edolstra.gpg | |
EDOLSTRA_GPG = """-----BEGIN PGP PUBLIC KEY BLOCK----- | |
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 | |
qXy2HtxyhCn9+V8ki5G/znEJwor48g== | |
=Mc2q | |
-----END PGP PUBLIC KEY BLOCK----- | |
""" | |
@dataclasses.dataclass(frozen=True) | |
class NixRelease: | |
system: str | |
version: str | |
@property | |
def release_url(self) -> str: | |
return f"https://nixos.org/releases/nix/nix-{self.version}" | |
@property | |
def base_name(self) -> str: | |
return f"nix-{self.version}-{self.system}" | |
@property | |
def archive_name(self) -> str: | |
return f"{self.base_name}.tar.bz2" | |
@property | |
def sig_name(self) -> str: | |
return f"{self.archive_name}.asc" | |
@property | |
def archive_url(self) -> str: | |
return f"{self.release_url}/{self.archive_name}" | |
@property | |
def sig_url(self) -> str: | |
return f"{self.release_url}/{self.sig_name}" | |
if __name__ == "__main__": | |
nix_path = pathlib.Path("/nix") | |
user_numeric = os.getuid() | |
group_numeric = os.getgid() | |
user_name = pwd.getpwuid(user_numeric).pw_name | |
group_name = grp.getgrgid(group_numeric).gr_name | |
prereq_message = f"""\ | |
This script does not want to run as root. Please create the /nix directory | |
and ensure it is owned by the current user before running. | |
WARNING: the commands below remove your current nix store if you have one | |
already. Please consider carefully whether you want this. | |
sudo rm -rf /nix | |
sudo mkdir -p /nix | |
sudo chown {user_name}:{group_name} -R /nix""" | |
if not nix_path.exists(): | |
print("The /nix directory does not exist\n") | |
print(dedent(prereq_message)) | |
sys.exit(1) | |
nix_stat = os.stat(nix_path) | |
if (nix_stat.st_uid != user_numeric) or (nix_stat.st_gid != group_numeric): | |
print("The /nix directory has wrong permissions\n") | |
print(dedent(prereq_message)) | |
sys.exit(1) | |
nix_store_dir = pathlib.Path("/nix/store") | |
release = NixRelease(version="2.2.2", system="x86_64-linux") | |
with tempfile.TemporaryDirectory() as t: | |
temp_dir = pathlib.Path(t) | |
tar_file = temp_dir / release.archive_name | |
pubkey_asc_file = temp_dir / "edolstra.asc" | |
pubkey_file = f"{pubkey_asc_file}.gpg" | |
sig_asc_file = temp_dir / release.sig_name | |
sig_file = f"{sig_asc_file}.gpg" | |
extract_dir = temp_dir / release.base_name | |
print(f"Downloading Nix release from {release.archive_url}") | |
with tar_file.open(mode="wb") as tf: | |
response = request.urlopen(release.archive_url) | |
tf.write(response.read()) | |
with pubkey_asc_file.open(mode="w") as pf: | |
pf.write(EDOLSTRA_GPG) | |
with sig_asc_file.open(mode="wb") as sf: | |
response = request.urlopen(release.sig_url) | |
sf.write(response.read()) | |
print(f"Verifying GPG signature") | |
subprocess.run(["gpg", "--dearmor", str(pubkey_asc_file)], check=True) | |
subprocess.run(["gpg", "--dearmor", str(sig_asc_file)], check=True) | |
subprocess.run( | |
[ | |
"gpg", | |
"--no-default-keyring", | |
"--keyring", | |
str(pubkey_file), | |
"--verify", | |
str(sig_file), | |
str(tar_file), | |
], | |
check=True, | |
) | |
with tarfile.open(tar_file) as tar: | |
reginfo_member = None | |
store_members = [] | |
for member in tar: | |
store_prefix = f"{release.base_name}/store/" | |
if member.name.startswith(store_prefix): | |
member.name = member.name[len(store_prefix) :] | |
store_members.append(member) | |
if member.name.endswith(".reginfo"): | |
reginfo_member = member | |
assert len(store_members) > 0 | |
assert reginfo_member is not None | |
tar.extractall(path=extract_dir, members=store_members) | |
reginfo_contents = tar.extractfile( # type: ignore | |
reginfo_member | |
).read() | |
extracted_nix_dir = next( | |
pathlib.Path(entry.name) | |
for entry in extract_dir.iterdir() | |
if entry.name.endswith(f"nix-{release.version}") | |
) | |
nix_store_dir.mkdir() | |
for entry in extract_dir.iterdir(): | |
shutil.move(str(entry), str(nix_store_dir)) | |
for entry in nix_store_dir.iterdir(): | |
subprocess.run(["chmod", "-R", "a-w", str(entry)], check=True) | |
# Unsure what this does. No stuff in usage or the manpage. The source code | |
# contains a comment "DB is loaded automatically". Let's call it to be sure? | |
subprocess.run( | |
[str(nix_store_dir / extracted_nix_dir / "bin/nix-store"), "--init"], | |
check=True, | |
) | |
# This command sets internal Nix state in the SQLite database. The reginfo | |
# file apparently contains derivations, their expected hashes on disk, and | |
# their required sizes. Without this, the nix store cannot detect faults. | |
# Normal builds also add to the ValidPaths table of the store. | |
subprocess.run( | |
[str(nix_store_dir / extracted_nix_dir / "bin/nix-store"), "--load-db"], | |
check=True, | |
input=reginfo_contents, | |
) | |
print( | |
dedent( | |
f"""\ | |
Nix installation completed. | |
Please add the following to your shell config to update your PATH: | |
export PATH="{nix_store_dir}/{extracted_nix_dir}/bin/:$PATH" | |
""" | |
) | |
) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment