Skip to content

Instantly share code, notes, and snippets.

@duijf
Last active September 24, 2019 16:49
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save duijf/45d0a636396761cf279ad84e1598fa56 to your computer and use it in GitHub Desktop.
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.
#!/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