#!/usr/bin/perl

# apt-repo -- Manipulate APT repository list
# $Id: apt-repo,v 1.3.7 2016-06-08 11:50:00 cas Exp $

# Copyright 2011-2016 by Andrey Cherepanov (cas@altlinux.org)
# Copyright 2015 by Ivan Zakharyaschev (imz@altlinux.org)
# (imz: support for relying on apt-config and APT_CONFIG)
# (imz: Support for local --hsh-apt-config=FILE together with hasher.)

# This program is free software; you can redistribute it and/or modify it
# under the terms of GNU General Public License (GPL) version 3 or later.

use strict;
use warnings;

# Default parameters
our $VERSION = '1.3.9';

my $type     = 'rpm';
my $c_branch = 'classic';
my $c_task   = 'task';
my $noarch   = 'noarch';

my $cmd      = 'list';
my $continues = 0;
my $hsh = 0;
if ( scalar @ARGV > 0 and $ARGV[0] =~ /^--hsh-apt-config=(.*)$/ ) {
    # Printing is useful for debugging, too:
    warn "Info: Will use hasher when appropriate (with --apt-config=$1).";
    $ENV{APT_CONFIG} = $1;
    $hsh = 1;
    shift;
}
$cmd = $ARGV[0] if scalar @ARGV > 0;

# Get system arch
my $arch = `/bin/uname -m`;
chomp $arch; # Truncate carriage return from output
# arch for x86_32
$arch = 'i586' if $arch =~ /^i686$/;

# Default repository paths
my $repo_base = 'http://ftp.altlinux.org/pub/distributions/ALTLinux';
my $repo_task = 'http://git.altlinux.org/repo/';
my $conf_main = '/etc/apt/sources.list';
my $conf_list = '/etc/apt/sources.list.d/*.list';
if ( defined $ENV{APT_CONFIG} ) {
    # The following expression is intentionally crazily complex,
    # because we want to debug how children see the environment variable.
    warn 'Info: Will try to read a non-system APT_CONFIG=' . `echo -nE "\$APT_CONFIG"`;
    if ( $ENV{APT_CONFIG} =~ /^~/ ) {
	warn 'Warning: Your APT_CONFIG begins with ~, but no tilde expansion will take place';
    }
}
# If APT is missing in the system, we fallback to the common defaults above.
# Otherwise, we get the paths from APT itself:
# (if `apt-config` is missing, the pattern below won't match.)
if ( `apt-config shell FILE Dir::Etc::sourcelist/f` =~ /^FILE=(.*)$/ ) {
    # We are lucky that `apt-config` has the /f and /d "switches" which
    # make it resolve the configuration parameters into "normalized"
    # full paths to files and dirs.

    # $1 is a string quoted for the shell (with quotation marks around it).
    # Therefore we rely on shell itself to print it cleanly:
    $conf_main = `echo -nE $1`;
    # Invalidate the default because we want consistent values from `apt-config`: 
    $conf_list = '';
    if ( `apt-config shell DIR Dir::Etc::sourceparts/d` =~ /^DIR=(.*)$/ ) {
	$conf_list = `echo -nE $1` . "*.list";
    } else {
	warn "Warning: Getting Dir::Etc::sourceparts/d from `apt-config` went wrong; falling back to '$conf_list' instead."
    }
} else {
    warn "Warning: Getting Dir::Etc::sourcelist/f from `apt-config` went wrong; falling back to '$conf_main' instead."
}

my %branches  = (
	'4.0' => [ "$repo_base/4.0/branch", "updates", "classic" ],
	'4.1' => [ "$repo_base/4.1/branch", "updates", "classic" ],
	'5.0' => [ "$repo_base/5.0/branch", "updates", "classic" ],
	'p5'  => [ "$repo_base/p5/branch",  "updates", "classic" ],
	'5.1' => [ "$repo_base/5.1/branch", "updates", "classic" ],
	'p6'  => [ "$repo_base/p6/branch",  "updates", "classic" ],
	't6'  => [ "$repo_base/t6/branch",  "updates", "classic" ],
	'c6'  => [ "$repo_base/c6/branch",  "updates", "classic" ],
	'p7'  => [ "$repo_base/p7/branch",  "updates", "classic" ],
	't7'  => [ "$repo_base/t7/branch",  "updates", "classic" ],
	'c7'  => [ "$repo_base/c7/branch",  "updates", "classic" ],
	'p8'  => [ "$repo_base/p8/branch",  "updates", "classic" ],
	'c8'  => [ "$repo_base/c8/branch",  "updates", "classic" ],
	'sisyphus' => [ "$repo_base/Sisyphus", "alt", "classic" ],
	'Sisyphus' => [ "$repo_base/Sisyphus", "alt", "classic" ],
	'autoimports.p7' => [ "http://ftp.altlinux.ru/pub/distributions/ALTLinux/autoimports/p7", "", "autoimports" ],
	'autoimports.p8' => [ "http://ftp.altlinux.ru/pub/distributions/ALTLinux/autoimports/p8", "", "autoimports" ],
	'autoimports.sisyphus' => [ "http://ftp.altlinux.ru/pub/distributions/ALTLinux/autoimports/Sisyphus", "", "autoimports" ],
	'altlinuxclub.4.0' => [ "http://altlinuxclub.ru/repo/Repo_4/",  "", "hasher" ],
	'altlinuxclub.p5'  => [ "http://altlinuxclub.ru/repo/Repo_P5/", "", "hasher" ],
	'altlinuxclub.p6'  => [ "http://altlinuxclub.ru/repo/Repo_P6/", "", "hasher" ],
	'altlinuxclub.p7'  => [ "http://altlinuxclub.ru/repo/Repo_P7/", "", "hasher" ],
	'altlinuxclub.p8'  => [ "http://altlinuxclub.ru/repo/Repo_P8/", "", "hasher" ],
	'altlinuxclub.sisyphus' => [ "http://altlinuxclub.ru/repo/repo_s/",  "", "hasher" ]
);

# Show usage information
sub show_usage {
	print <<"HELP";
Usage: apt-repo [--hsh-apt-config=FILE] COMMAND SOURCE
Manipulate APT repository list.

COMMANDS:
  list [-a]        List active or all repositories
  list [task] <id> List task packages
  add <source>     Add APT repository
  rm <source|all>  Remove APT repository
  clean            Remove all cdrom and task sources
  update           Update APT caches
                   (Runs `hsh --initroot-only` if `--hsh-apt-config` is given.)
  test [task] <id> [pkg1 ...] Install all packages (except *-debuginfo) from task
                   (Uses hasher(7) if `--hsh-apt-config` is given.)
                   You can optional specify package(s) to test.
  test [task] '' pkg1 ... Install the packages (without modifying APT repos)
                   (Uses hasher(7) if `--hsh-apt-config` is given.)
  -h, --help       Show help and exit
  -v               Show version and exit

<source> may be branch or task name, sources.list(5) string, URL or local path.

The files to be manipulated are determined by a call to apt-config(8).
Consequently, APT_CONFIG environment variable can be used to point
to arbitrary non-system configurations for APT.

There is a switch for the special case when you want to use hasher(7) together
with a local APT configuration: --hsh-apt-config=FILE.
Note that no tilde expansion is performed on FILE!
HELP
	exit 0;
}

# Show version
sub show_version {
	print "$VERSION\n";
	exit 0;
}

# Return array of file names with sources
sub get_source_files {
	my $dir;
	my @files = ();

	# Build files list
	push @files, $conf_main if -e $conf_main;
	$dir = substr( $conf_list, 0, -6 );
	opendir( DIR, substr( $conf_list, 0, -6 ) );
	foreach my $name (sort readdir( DIR )) {
		push @files, $dir . $name if $name =~ /\.list$/;
	}
	closedir( DIR );
	return @files;
}

# Return list of repositories as text
sub get_repos {
	my $all = shift;
	my @output = ();
	my $name;

	# Iterate through @files
	foreach $name ( get_source_files() ) {
		open(my $file, "<", $name) or warn "Can't open file '$name'";
		while( my $l = <$file> ) {
			# Show all active repositories
			push( @output, $l ) if $l =~ /^[[:space:]]*$type(-src|-dir)?[[:space:]]+/;
			# On -a show all available commented repositories
			push( @output, $l ) if defined $all and $all =~ /^-a$/ and $l =~ /^[[:space:]]*#[[:space:]]*$type(-src|-dir)? /;
		}
		close( $file );
	}
	return @output;
}

# Get package list for task
sub get_task_content {
	my $task = shift;
	my @out = ();

	die "Missing or wrong task number" if ! defined $task or ! $task=~ /^(\d+)$/;

	open P, '-|', "curl -s http://git.altlinux.org/tasks/$task/plan/add-bin | cut -f1 | grep -v '\\-debuginfo\$' | sort -u";
	@out = <P>;
	close P;
	return @out;
}

# Get status of arepo for task
sub task_has_arepo {
	my $task = shift;
	my @out = ();

	die "Missing or wrong task number" if ! defined $task or ! $task=~ /^(\d+)$/;

	open P, '-|', "curl -s  -w '%{http_code}' http://git.altlinux.org/tasks/$task/plan/arepo-add-x86_64-i586";
	@out = <P>;
	close P;
	return ( (pop @out) eq "200" and (scalar @out) != 0);
}

# Show repositories
sub show_repo {
	shift;
	my $all = shift;

	# List for task packages by task number or canonical form `task <number>`
	if( defined $all and $all =~ /^(\d+|task)$/ ) {
		$all = shift if $all =~ /^task$/;
		print join( "",get_task_content( $all ) );
	} else {
		print get_repos( $all );
	}

	exit 0;
}

# Convert source to new format to show last the dir in apt-get output
sub new_format {
	my $source = shift;
        my @s = split ' ', $source;
        my $i = 1;

        # move two last path components to architecture
        $i++ if $s[$i] =~ /^\[/;
        $s[$i] =~ s/(.*)\/([^\/]+\/[^\/]+)\/?/${1}/;
        if( defined $2 and $2 ne '') {
		$s[$i+1] = $2 . '/' . $s[$i+1];
        }

        return join( ' ', @s );
}

# Determine repository URL
sub get_url {
	my $repo   = shift;
	my $object = shift;
	my @url    = @_;
	my $u = '';
	my $k;
	my @branch_source;

	defined $repo or die "Unknown source. See `man apt-repo` for details.";

	# Quick forms: known branch name or number for task
	if( exists $branches{ $repo } ) {
		$object = $repo;		
		$repo = 'branch';
	}

	if( $repo =~ /^[0-9]+$/ ) {
		$object = $repo;		
		$repo = 'task';
	}

	# Branch 	
	if( $repo eq 'branch' ) {
		# Branch list
		if ( not defined $object )  {
			# Show all available branch names
			foreach $k (sort keys %branches) {
				print $k . "\n";
			}
			exit 0;
		}

		# URL for branch
		if( exists $branches{ $object } ) {
			my $key = '';
			my $farch = $arch;
			my $altlinuxclub = 0;
			my $autoimports  = 0;
			my $url = $branches{ $object }[0];

			$altlinuxclub = 1 if $url =~ /^http\:\/\/altlinuxclub\.ru/;
			$autoimports  = 1 if $url =~ /\/ALTLinux\/autoimports\//;

			# Hack $arch for altlinuxclub.ru
			$farch = 'i686' if $altlinuxclub and $arch eq 'i586';

			if( $branches{ $object }[1] ne "" ) {
				$key = '[' . $branches{ $object }[1] . '] ';
			}
			$u = 'rpm ' . $key . $url;

			# Sources list
			@branch_source = ( $u . ' ' . $farch . ' ' . $branches{ $object }[2] );
			push( @branch_source, $u . ' ' . $noarch . ' ' . $branches{ $object }[2] ) if not $altlinuxclub;

			# For x86_64 add Arepo 2.0 source
			if( $farch eq 'x86_64' and not $altlinuxclub and not $autoimports ) {
				push( @branch_source, $u . ' x86_64-i586 ' . $branches{ $object }[2] );
			}
		} else {
			print "Unknown branch name '$object'\n";
			exit 1;
		}
		#print join("\n", @branch_source), "\n";
		return @branch_source;

	}

	# Task
	if( $repo eq 'task' ) {
		if( not defined $object ) {
			print "Task number is missed.\n";
			exit 1;
		}
		my @ret = ( 'rpm ' . $repo_task . $object . '/ ' . $arch . ' ' . $c_task );
		if( $arch eq 'x86_64' and ( $ARGV[0] eq 'rm' or  task_has_arepo( $object ) ) ) {
			# Arepo source for x86_64
			push @ret, 'rpm ' . $repo_task . $object . '/ x86_64-i586 ' . $c_task;
		}
		return @ret;
	}

	# URL
	if( $repo =~ /^(http|ftp|rsync|file|copy|cdrom):\// ) {
		my $u = 'rpm ' . $repo;
		my $component = $c_branch;

		if( defined $object ) {
			# Architecture is defined
			return ( $u . ' ' . $object . ' ' . join( ' ', @url ) );
		} else {
			# Mirror
			return (
				$u . ' ' . $arch . ' ' . $component, 
				$u . ' ' . $noarch . ' ' . $component
			);
		}
	}

	# Absolute path for hasher repository
	if( $repo =~ /^\// ) {
		return ( 'rpm file://' . $repo . ' ' . $arch . ' hasher' );
	}

	# In format of sources.list(5)
	if( $repo =~ /^$type(-src|-dir)?\b/ ) {
		if( defined $object ) {
			return ( $repo . ' ' . $object . ' ' . join( ' ', @url ) );
		} else {
			return ( $repo );
		}
	}

	return ();
}

# Add repository to list
sub add_repo {
	shift;
	my $repo   = shift;
	my $object = shift;
	my @comps  = @_;
	my $a_found;
	my $i_found;
	my @c = ();

	my @urls = get_url( $repo, $object, @comps );

	if( scalar @urls == 0 ) {
		print "Nothing to add: bad source format. See `man apt-repo` for details.\n";
		exit 1;
	}

	foreach my $u ( @urls ) {
		my $unew = new_format( $u );
		$a_found = 0;
		$i_found = 0;

		#print "added $u\n";

		# Lookup active ones
		foreach( get_repos( '-a' ) ) {
			chomp;
			# Check active ones
			if( /^\Q$u\E$/ or /^\Q$unew\E$/ ) {
				# This source is active 
				$a_found = 1;
				last;
			}
			# Check commented ones
			if( /^[[:space:]]*#[[:space:]]*\Q$u\E$/ or /^[[:space:]]*#[[:space:]]*\Q$unew\E$/ ) {
				# This source is existing and commented
				$i_found = 1;
				last;
			}
		}

		#print "$a_found $i_found $u\n";

		# Process source
		next if $a_found; # Source is active, nothing do

		if( $i_found ) {
			my $ret_file;
			# Uncomment commented source
			my $ur = quotemeta $u;
			my $urnew = quotemeta $unew;
			# Iterate through config files
			foreach my $name ( get_source_files() ) {
				open( FILE, "<", $name ); @c = ( <FILE> ); close FILE;
				foreach ( @c ) {
					s/^[[:space:]]*#[[:space:]]*($ur)$/${1}/;
					s/^[[:space:]]*#[[:space:]]*($urnew)$/${1}/;
				}
				$ret_file = open( FILE, ">", $name );
				if ( not defined $ret_file ) {
					print "Unable to edit file $name. Possibly, permission denied. Exiting.\n";
					exit 1;
				}
				if ( $ret_file != 0 ) {
					print FILE @c;
					close FILE;
				}
			}
			next;
		} else {
			# Append to main sources list file
			open CONFIG, '>>', "$conf_main" or die "Can't open $conf_main: $!";
			print CONFIG $unew . "\n";
			close CONFIG;
		}
	}

	exit 0 if ! $continues;
}

# Remove repository from list
sub rm_repo {
	shift;
	my $repo   = shift;
	my $object = shift;
	my @comps  = @_;
	my $a_found;
	my $i_found;
	my @c = ();

	my @urls;

	if( defined $repo and $repo eq 'all' ) {
		# Remove all active sources
		@urls = get_repos();

		# Remove repositories by specified type
		if( defined $object ) {
			$object =~ /^(branch|branches|task|tasks|cdrom|cdroms)$/ or die "Missing repository type for `apt-repo rm all <type>`";
			@urls = grep( /[[:space:]]+classic$/, @urls ) if( $object =~ /^branch(es)?$/ );
			@urls = grep( /[[:space:]]+task$/, @urls ) if( $object =~ /^task(s)?$/ );
			@urls = grep( /^[[:space:]]*rpm[[:space:]]+cdrom:/, @urls ) if( $object =~ /^cdrom(s)?$/ );
		}

	} else {
		@urls = get_url( $repo, $object, @comps );
	}

	foreach my $u ( @urls ) {
		my $unew = new_format( $u );
		$a_found = 0;
		$i_found = 0;

		chomp $u;

		# Lookup active
		foreach( get_repos() ) {
			chomp;
			# Check active
			if( /^\Q$u\E$/ or /^\Q$unew\E$/ ) {
				# This source is active 
				$a_found = 1;
				last;
			}
		}

		#print "$a_found $u\n";

		# Remove from $conf_main, comment in other files
		if( $a_found ) {
			my $ret_file;
			my @cc;
			$u = quotemeta $u;
			$unew = quotemeta $unew;
			# Iterate through config files
			open( FILE, "<", $conf_main ); @c = ( <FILE> ); close FILE;
			@cc = grep {!/^[[:space:]]*$u$/} @c;
			@cc = grep {!/^[[:space:]]*$unew$/} @cc;
			$ret_file = open( FILE, ">", $conf_main );
			if ( not defined $ret_file ) {
				print "Unable to edit file $conf_main. Possibly, permission denied. Exiting.\n";
				return 1;
			}
			if ( $ret_file != 0 ) {
				print FILE @cc;
				close FILE;
			}
			# Comment in config files
			foreach my $name ( get_source_files() ) {
				open( FILE, "<", $name ); @c = ( <FILE> ); close FILE;
				foreach ( @c ) {
					s/^[[:space:]]*($u)$/#${1}/;
					s/^[[:space:]]*($unew)$/#${1}/;
				}
				open( FILE, ">", $name ) or next; print FILE @c; close FILE;
			}
		}
	}
	return 0;
}

# Remove all cdrom and task repositories
sub clear_repo {
	my @cmd;

	@cmd = qw(rm all cdroms);
	rm_repo( @cmd );

	@cmd = qw(rm all tasks);
	rm_repo( @cmd );

	exit 0 if ! $continues;
}

# Update repo
sub update_repo {
    my $error_code = 0;
    if (! $hsh ) {
	$error_code = system 'apt-get update';
    } else {
	# Normally, hsh --initroot-only prepares an environment with rpm-build etc. (for building packages).
	# Instead, here, we do as girar's install check does, which prepares a smaller minimal system:
	# http://git.altlinux.org/people/ldv/packages/?p=girar.git;a=blob;f=gb/remote/gb-remote-check-install;h=e7823af17cdfc68369f5782a8cdbac18e581adb7;hb=1535653ca9923ea8cd228ae68b2ca7c1b80eba7e#l41
	# This would allow us to reproduce the bugs that happen in girar's install check, like that one: https://bugzilla.altlinux.org/show_bug.cgi?id=31576 .
	# TODO:
	# The difference is that girar explicitly knows that target repo branch (like Sisyphus, p7, etc.), and hence is able to use an explicit altlinux-release-sisyphus package.
	# We simply rely on the virtual package. (Which is not very nice, because it can install a different special package.)
	$error_code = system "hsh --apt-config=\"\$APT_CONFIG\" --initroot-only --pkg-init-list=+altlinux-release --pkg-build-list=altlinux-release,basesystem";
    }
    if ( $error_code ) {
	warn 'Error: The "update" command has not completed successfully';
    }
    exit $error_code if ! $continues;
    return !$error_code;
}

# Test task
sub test_task {
	shift;
	my $task = shift;
	my @pkgs = ();
	my $list = "";

	$task = shift if $task eq 'task';

	@pkgs = @_;
	if ( scalar(@pkgs) == 0 ) {
		@pkgs = get_task_content( $task );
	}

	# Add source and update indices
	$continues = 1;
	if ( not $task eq '' ) {
	    add_repo( ("add", "task", $task ) );
	}
	if ( update_repo() or ! $hsh ) {
	    # `hsh --initroot-only` is particularly fragile,
	    # so if we are using hsh and if the "update" command
	    # hasn't completed successfully, we abort.

	# Install all packages from task (except *-debuginfo)
	chomp( @pkgs );
	$list = join( " ", @pkgs );

	# Install packages from task repository
	if (! $hsh ) {
	    system "cd /var/empty/; apt-get install $list";
	} else {
	    system "cd /var/empty/; hsh-install $list";
	}

	}

	# Remove task source
	if ( not $task eq '' ) {
	    rm_repo( ("rm", "task", $task ) );
	}

	exit 0;
}

# Process command line arguments

# Exiting functions
show_repo( @ARGV ) if $cmd eq 'list';
show_repo( 'list', '-a' ) if $cmd eq '-a';
show_usage() if $cmd =~ /-(h|-help)$/ or scalar @ARGV == 0; 
show_version() if $cmd =~ /-(v|-version)$/;
clear_repo( @ARGV ) if $cmd =~ /^clea[rn]$/;
update_repo() if $cmd eq 'update';
test_task( @ARGV ) if $cmd eq 'test';

# Functions with return
if( $cmd =~ /^(add|rm)$/ ) {
	add_repo( @ARGV ) if $cmd eq 'add';
	rm_repo( @ARGV ) if $cmd eq 'rm';
} else {
	print "Unknown command `$cmd`.\nRun `apt-repo --help` for supported commands.\n";
	exit 1;
}

__END__
