#! @perl@ -w @perlFlags@ use utf8; use strict; use File::Basename; use File::Path qw(mkpath); use Nix::Config; use Nix::Manifest; use File::Temp qw(tempdir); binmode STDERR, ":encoding(utf8)"; Nix::Config::readConfig; # Turn on caching in nix-prefetch-url. my $channelCache = "$Nix::Config::stateDir/channel-cache"; mkdir $channelCache, 0755 unless -e $channelCache; $ENV{'NIX_DOWNLOAD_CACHE'} = $channelCache if -W $channelCache; # Figure out the name of the `.nix-channels' file to use. my $home = $ENV{"HOME"} or die '$HOME not set\n'; my $channelsList = "$home/.nix-channels"; my $nixDefExpr = "$home/.nix-defexpr"; # Figure out the name of the channels profile. my $userName = getpwuid($<) || $ENV{"USER"} or die "cannot figure out user name"; my $profile = "$Nix::Config::stateDir/profiles/per-user/$userName/channels"; mkpath(dirname $profile, 0, 0755); my %channels; # Reads the list of channels. sub readChannels { return if (!-f $channelsList); open CHANNELS, "<$channelsList" or die "cannot open ‘$channelsList’: $!"; while (<CHANNELS>) { chomp; next if /^\s*\#/; my ($url, $name) = split ' ', $_; $url =~ s/\/*$//; # remove trailing slashes $name = basename $url unless defined $name; $channels{$name} = $url; } close CHANNELS; } # Writes the list of channels. sub writeChannels { open CHANNELS, ">$channelsList" or die "cannot open ‘$channelsList’: $!"; foreach my $name (keys %channels) { print CHANNELS "$channels{$name} $name\n"; } close CHANNELS; } # Adds a channel. sub addChannel { my ($url, $name) = @_; die "invalid channel URL ‘$url’" unless $url =~ /^(file|http|https):\/\//; die "invalid channel identifier ‘$name’" unless $name =~ /^[a-zA-Z0-9_][a-zA-Z0-9_\-\.]*$/; readChannels; $channels{$name} = $url; writeChannels; } # Remove a channel. sub removeChannel { my ($name) = @_; readChannels; my $url = $channels{$name}; delete $channels{$name}; writeChannels; system("$Nix::Config::binDir/nix-env --profile '$profile' -e '$name'") == 0 or die "cannot remove channel ‘$name’\n"; } # Fetch Nix expressions and binary cache URLs from the subscribed channels. sub update { my @channelNames = @_; readChannels; # Download each channel. my $exprs = ""; foreach my $name (keys %channels) { next if scalar @channelNames > 0 && ! grep { $_ eq $name } @{channelNames}; my $url = $channels{$name}; # We want to download the url to a file to see if it's a tarball while also checking if we # got redirected in the process, so that we can grab the various parts of a nix channel # definition from a consistent location if the redirect changes mid-download. my $tmpdir = tempdir( CLEANUP => 1 ); my $filename; ($url, $filename) = `cd $tmpdir && $Nix::Config::curl --silent --write-out '%{url_effective}\n%{filename_effective}' -L '$url' -O`; chomp $url; die "$0: unable to check ‘$url’\n" if $? != 0; # If the URL contains a version number, append it to the name # attribute (so that "nix-env -q" on the channels profile # shows something useful). my $cname = $name; $cname .= $1 if basename($url) =~ /(-\d.*)$/; my $path; my $ret = -1; if (-e "$tmpdir/$filename" && $filename =~ /\.tar\.(gz|bz2|xz)$/) { # Get our temporary download into the store. (my $hash, $path) = `PRINT_PATH=1 QUIET=1 $Nix::Config::binDir/nix-prefetch-url 'file://$tmpdir/$filename'`; chomp $path; # Try unpacking the expressions to see if they'll be valid for us to process later. # Like anything in nix, this will cache the result so we don't do it again outside of the loop below. $ret = system("$Nix::Config::binDir/nix-build --no-out-link -E 'import <nix/unpack-channel.nix> " . "{ name = \"$cname\"; channelName = \"$name\"; src = builtins.storePath \"$path\"; }'"); } # The URL doesn't unpack directly, so let's try treating it like a full channel folder with files in it my $extraAttrs = ""; if ($ret != 0) { # Check if the channel advertises a binary cache. my $binaryCacheURL = `$Nix::Config::curl --silent '$url'/binary-cache-url`; $extraAttrs .= "binaryCacheURL = \"$binaryCacheURL\"; " if $? == 0 && $binaryCacheURL ne ""; # Download the channel tarball. my $fullURL = "$url/nixexprs.tar.xz"; system("$Nix::Config::curl --fail --silent --head '$fullURL' > /dev/null") == 0 or $fullURL = "$url/nixexprs.tar.bz2"; print STDERR "downloading Nix expressions from ‘$fullURL’...\n"; (my $hash, $path) = `PRINT_PATH=1 QUIET=1 $Nix::Config::binDir/nix-prefetch-url '$fullURL'`; die "cannot fetch ‘$fullURL’\n" if $? != 0; chomp $path; } # Regardless of where it came from, add the expression representing this channel to accumulated expression $exprs .= "'f: f { name = \"$cname\"; channelName = \"$name\"; src = builtins.storePath \"$path\"; $extraAttrs }' "; } # Unpack the channel tarballs into the Nix store and install them # into the channels profile. print STDERR "unpacking channels...\n"; system("$Nix::Config::binDir/nix-env --profile '$profile' " . "-f '<nix/unpack-channel.nix>' -i -E $exprs --quiet") == 0 or die "cannot unpack the channels"; # Make the channels appear in nix-env. unlink $nixDefExpr if -l $nixDefExpr; # old-skool ~/.nix-defexpr mkdir $nixDefExpr or die "cannot create directory ‘$nixDefExpr’" if !-e $nixDefExpr; my $channelLink = "$nixDefExpr/channels"; unlink $channelLink; # !!! not atomic symlink($profile, $channelLink) or die "cannot symlink ‘$channelLink’ to ‘$profile’"; } die "$0: argument expected\n" if scalar @ARGV == 0; while (scalar @ARGV) { my $arg = shift @ARGV; if ($arg eq "--add") { die "$0: ‘--add’ requires one or two arguments\n" if scalar @ARGV < 1 || scalar @ARGV > 2; my $url = shift @ARGV; my $name = shift @ARGV; unless (defined $name) { $name = basename $url; $name =~ s/-unstable//; $name =~ s/-stable//; } addChannel($url, $name); last; } if ($arg eq "--remove") { die "$0: ‘--remove’ requires one argument\n" if scalar @ARGV != 1; removeChannel(shift @ARGV); last; } if ($arg eq "--list") { die "$0: ‘--list’ requires one argument\n" if scalar @ARGV != 0; readChannels; foreach my $name (keys %channels) { print "$name $channels{$name}\n"; } last; } elsif ($arg eq "--update") { update(@ARGV); last; } elsif ($arg eq "--rollback") { die "$0: ‘--rollback’ has at most one argument\n" if scalar @ARGV > 1; my $generation = shift @ARGV; my @args = ("$Nix::Config::binDir/nix-env", "--profile", $profile); if (defined $generation) { die "invalid channel generation number ‘$generation’" unless $generation =~ /^[0-9]+$/; push @args, "--switch-generation", $generation; } else { push @args, "--rollback"; } system(@args) == 0 or exit 1; last; } elsif ($arg eq "--help") { exec "man nix-channel" or die; } elsif ($arg eq "--version") { print "nix-channel (Nix) $Nix::Config::version\n"; exit 0; } else { die "unknown argument ‘$arg’; try ‘--help’\n"; } }