Files
snap/SRC/snap/Snap.pm
Jay Larson 6b62f87bf6 The following changes were made:
* Cleaned up some output and variable assignments
  * Added additional error handling/output
  * Implemented 'source' command (git clones)
  * More variable requirements for Makefile.snaplinux
  * Package.pm is now more sane in dealing with the package object
  * Added chroot/fakechroot (will likely need further tweaking!)
  * Improved locking
  * Added ability to use templates for snapinstall (will be expanded!)
2018-01-17 09:25:06 -06:00

988 lines
20 KiB
Perl

package Snap;
use strict;
use warnings;
use Snap::Commands;
use Snap::Package;
use Snap::Sources;
use Fcntl qw( :flock );
use IPC::Open3;
use IO::Socket::INET;
use Digest::SHA qw( sha256_hex );
use POSIX;
use Data::Dumper;
use parent 'Exporter';
our @EXPORT = qw(
chkyes
error
genpkg
httpget
human
list
listfiles
virtfs
readconf
refresh
setup
sha
sha256
target
termsize
vercmp
);
use constant DEBUG => eval {
for ( my $i = 0; $i <= $#ARGV; $i++ ) {
if ( $ARGV[$i] eq '--debug' ) {
splice( @ARGV, $i, 1 );
return( 1 );
}
}
return( 0 );
};
use constant TARGET => eval {
my $target = '';
for ( my $i = 0; $i <= $#ARGV; $i++ ) {
if ( $ARGV[$i] eq '-t' ) {
$target = $ARGV[$i+1];
splice( @ARGV, $i, 2 );
}
elsif ( $ARGV[$i] =~ /^-t(\S+)/ ) {
$target = $1;
splice( @ARGV, $i, 1 );
}
}
$target =~ s/(\/+){2}/\//g;
$target =~ s/\/$//;
return( $target );
};
use constant CONFFILE => eval {
if ( -f TARGET . '/etc/snap.conf' ) {
return( TARGET . '/etc/snap.conf' );
}
elsif ( -f '/etc/snap.conf' ) {
return( '/etc/snap.conf' );
}
else {
Snap->error( -1, 'No valid snap.conf found' );
}
};
use constant VERFILE => eval {
if ( -f TARGET . '/etc/snap_version' ) {
return( TARGET . '/etc/snap_version' );
}
elsif ( -f '/etc/snap_version' ) {
return( '/etc/snap_version' );
}
else {
Snap->error( -1, 'No valid snap_version found' );
}
};
use constant {
VERSION => '0.12',
SNAPDIR => TARGET . '/var/lib/snap',
PKGDIR => TARGET . '/var/lib/snap/packages',
INSTDIR => TARGET . '/var/lib/snap/installed',
SRCDIR => TARGET . '/var/lib/snap/sources'
};
use constant SNAPVER => eval {
my $version;
open( FILE, VERFILE ) || Snap->error( int( $! ), "open(): $!" );
$version = <FILE>;
close( FILE ) || Snap->error( int( $! ), "open(): $!" );
chomp( $version );
return( $version );
};
use constant LOCKFILE => TARGET . '/.snap';
############################################################
#
# Set the process name
#
############################################################
$0 =~ s/.*\///;
############################################################
#
# Make sure we bring back the cursor if we're killed
#
############################################################
$SIG{INT} = sub{
if ( TARGET ) {
virtfs( 'umount' );
}
print "\e[?25h\n";
exit( -1 );
};
############################################################
#
# Export TARGET to the environment for usher
#
############################################################
$ENV{TARGET} = TARGET;
sub chkreq {
my $class = shift;
my $req = shift;
my $version = shift;
if ( $req eq $version ) {
return( 1 );
}
elsif ( $req && $req =~ /^((<|>)=?|=)\s*(.*)/ ) {
my $op = $1;
my $ver = $3;
my $chk = Snap->vercmp( $version, $ver );
if ( $op &&
( $op eq '<' && $chk == -1 ) ||
( $op eq '<=' && $chk <= 0 ) ||
( $op eq '>' && $chk == 1 ) ||
( $op eq '>=' && $chk >= 0 ) ||
( $op eq '=' && $chk == 0 ) ) {
return( 1 );
}
}
return( 0 );
}
sub chkyes {
my $yes = '';
while( ! $yes ){
$yes = <STDIN>;
chomp( $yes );
if ( lc( $yes ) eq 'n' ){
print STDERR "\nAborting!\n";
exit( 1 );
}
elsif ( lc( $yes ne 'y' ) ){
print "Answer 'y' or 'n': ";
undef( $yes );
}
}
}
### error() ################################################
#
# All errors should be sent here. This sub takes a status
# and error string as args. The status code is used as
# the exit code. This also iterates through the call stack
# which is dumped to STDERR
#
############################################################
sub error {
my $class = shift;
my $status = shift;
my $errstr = shift;
my $level = 1;
my @stack = ();
chomp( $errstr );
print "\e[?25h\n";
print STDERR ( caller() )[1] .":\n $errstr at line "
. ( caller() )[2] . "\n";
if ( DEBUG ) {
if ( caller( $level ) ) {
print "\n=== Stack Trace ===\n";
}
while ( my @trace = caller( $level++ ) ) {
print STDERR " $trace[1]:\n"
. " $trace[3]() called at line $trace[2]\n";
}
}
print "\n";
if ( $status ) {
virtfs( 'umount' );
exit( $status );
}
}
### genpkg () ##############################################
#
# This sub generates a skeleton of directories and files
# that can be used as a starting point for creating a
# package.
#
############################################################
sub genpkg{
my $pkgname = shift;
my $skelfile = '/usr/share/snap/Makefile.skel';
my $snapreadme = "This is the directory where the manifest, snapinfo,\n"
. "and files.tar.gz files will be created. It is also\n"
. "where the usher file should be placed if it is\n"
. "required by the package. Any other files that need\n"
. "to be included could also be placed here.\n";
my $patchreadme = "Place any patch files here and preface each with a\n"
. "number indicating the order of execution. Patch\n"
. "files are expected to use a .patch extension.\n";
mkdir( $pkgname, 0755 ) ||
Snap->error( int( $! ), "mkdir: $pkgname: $!" );
mkdir( "$pkgname/SNAP", 0755 ) || Snap->error( int( $! ), $! );
mkdir( "$pkgname/SRC", 0755 ) || Snap->error( int( $! ), $! );
mkdir( "$pkgname/SRC/patches", 0755 ) || Snap->error( int( $! ), $! );
open( SKEL, "<$skelfile" ) || Snap->error( int( $! ), $! );
open( MAKEFILE, ">$pkgname/Makefile" ) || Snap->error( int( $! ), $! );
while ( <SKEL> ) {
print MAKEFILE $_;
}
close( MAKEFILE );
close( SKEL );
open( README, ">$pkgname/SNAP/README" ) || Snap->error( int( $! ), $! );
print README $snapreadme;
close( README );
open( README, ">$pkgname/SRC/patches/README" )
|| Snap->error( int( $! ), $! );
print README $patchreadme;
close( README );
}
sub httpget {
my $class = shift;
my $url = shift;
my $dest = shift;
my $mode = shift;
( my $host = $url ) =~ s/^https?:\/\/|\/.*//g;
( my $file = $url ) =~ s/.*$host//;
( my $filename = $url ) =~ s/.*\///;
my %httpget = (
'status' => 0,
'stdout' => '',
'stderr' => '',
'length' => 0,
'type' => '',
'dflag' => 0,
'pct' => 0
);
my $sock = IO::Socket::INET->new(
PeerAddr => $host,
PeerPort => 'http(80)',
Proto => 'tcp'
) || Snap->error( int( $! ), "IO::Socket::Inet->new(): $!" );
my $bytes = 0;
my @wheel = qw( - \ | / );
my $p = 0;
local $| = 1;
$sock->send( "GET $file HTTP/1.0\r\n" );
$sock->send( "Host: $host\r\n" );
$sock->send( "\r\n" );
if ( ! $mode ) {
$mode = '0644';
}
if ( $dest ) {
sysopen( DEST, $dest, O_RDWR|O_TRUNC|O_CREAT, $mode ) ||
Snap->error( int( $! ), "sysopen(): $dest: $!" );
print "\e[?25l";
}
while ( <$sock> ) {
if ( $dest ) {
$bytes += length( $_ );
}
if ( ! $httpget{'dflag'} ) {
if ( ! $httpget{'status'}
&& $_ =~ /^HTTP\S+\s(\d+)/ ) {
$httpget{'status'} = $1;
}
elsif ( ! $httpget{'date'}
&& $_ =~ /^Date:\s+(.*)/ ) {
$httpget{'date'} = $1;
}
elsif ( ! $httpget{'server'}
&& $_ =~ /^Server:\s+(.*)/ ) {
$httpget{'server'} = $1;
}
elsif ( ! $httpget{'lastmod'}
&& $_ =~ /^Last-Modified:\s+(.*)/ ) {
$httpget{'lastmod'} = $1;
}
elsif ( ! $httpget{'etag'}
&& $_ =~ /^ETag:\s+(.*)/ ) {
$httpget{'etag'} = $1;
}
elsif ( ! $httpget{'length'}
&& $_ =~ /Content-Length:\s+(\d+)/ ) {
$httpget{'length'} = $1;
}
elsif ( ! $httpget{'type'}
&& $_ =~ /Content-Type:\s+(\S+)/ ) {
$httpget{'type'} = $1;
}
elsif( $_ eq "\r\n" ) {
$httpget{'dflag'}++;
}
}
elsif ( $dest ) {
print DEST $_;
}
else {
$httpget{'stdout'} .= $_;
}
if ( $dest && $httpget{'length'} &&
$bytes < $httpget{'length'} ) {
if ( $bytes ) {
$httpget{'pct'} = int( $bytes /
$httpget{'length'} * 100 );
}
printf( "%-50.50s [%-20s] %s\r",
" $wheel[$p] Downloading $filename",
'*' x int( $httpget{'pct'} / 5 ),
"$httpget{'pct'}%" );
if ( $p >= $#wheel ) {
$p = 0;
}
else {
$p++;
}
}
}
if ( $dest ) {
printf( "%-50.50s [%-20s] %s\e[?25h\n",
" * Downloaded $filename",
'*' x 20,
'100%' );
close( DEST );
}
close( $sock );
chomp( $httpget{'stdout'} );
return( $httpget{'stdout'} );
}
sub human {
my $B = shift;
my $human;
if ( $B > 1099511627776 ) {
$human = sprintf( '%.02f', $B / ( 1024 ** 4 ) ) . 'TB';
}
elsif ( $B > 1073741824 ) {
$human = sprintf( '%.02f', $B / ( 1024 ** 3 ) ) . 'GB';
}
elsif ( $B > 1048576 ) {
$human = sprintf( '%.02f', $B / ( 1024 ** 2 ) ) . 'MB';
}
else {
$human = sprintf( '%.02f', $B / 1024 ) . 'KB';
}
return( $human );
}
sub ismountpoint {
my $dir = shift;
my $statA;
my $statB;
if ( ! -d $dir ) {
return( 0 );
}
$statA = ( stat( $dir ) )[0];
$statB = ( stat( "$dir/.." ) )[0];
if ( $statA == $statB ) {
return( 0 );
}
else {
return( 1 );
}
}
sub issnap {
my $class = shift;
my $file = shift;
my $lines = 0;
my $snapinfo = 0;
my $name = 0;
my $version = 0;
my $issnap = 0;
open( FILE, "<$file" ) || Snap->error( -1, "open(): $file: $!" );
while ( <FILE> ) {
if ( substr( $_, 0, 9 ) eq 'snapinfo/' ) {
$snapinfo++;
}
elsif ( substr( $_, 0, 5 ) eq 'name:' ) {
$name++;
}
elsif ( substr( $_, 0, 8 ) eq 'version:' ) {
$version++;
}
$lines++;
if ( $lines >= 12 ) {
last;
}
}
close( FILE ) || Snap->error( -1, "close(): $file: $!" );
if ( $snapinfo && $name && $version ) {
return( 1 );
}
return( 0 );
}
sub list {
my $listpackages = shift;
my $packages = {};
my $package = {};
opendir( DIR, INSTDIR ) || Snap->error( -1, "opendir(): "
. INSTDIR . ": $!" );
foreach my $dir ( sort { $a cmp $b } readdir( DIR ) ) {
if ( $dir =~ /^\.{1,2}$/ || ! -f INSTDIR . "/$dir/snapinfo" ||
$listpackages->[0] && ! grep( $dir =~ /$_/, @$listpackages ) ) {
next;
}
open( SNAPINFO, "<", INSTDIR . "/$dir/snapinfo" )
|| Snap->error( int( $! ), "open: $!" );
while ( <SNAPINFO> ) {
####################################
#
# Temporary fix!!! Will need to
# remove after all packages are
# corrected...
#
####################################
$_ =~ s/^package:/name:/;
if ( $_ =~ /^(\S+):\s+(.*)$/ ) {
$package->{$1} = $2;
}
}
$packages->{$dir} = Snap::Package->new( $package );
close( SNAPINFO );
}
close( DIR );
return( $packages );
}
sub listfiles {
my $packages = list();
my $listfiles = {};
foreach my $package ( @{$packages} ) {
my $manifest = Snap->INSTDIR
. "/$package->{'name'}/manifest";
open( MANIFEST, "<$manifest" ) ||
Snap->error( int( $! ), "open(): $manifest: $!" );
while ( <MANIFEST> ) {
my ( $shasum, $perms, $file ) = split( /\s/, $_ );
$listfiles->{$file}{'name'} = $package;
$listfiles->{$file}{'shasum'} = $shasum;
$listfiles->{$file}{'perms'} = $perms;
}
close( MANIFEST ) ||
Snap->error( int( $! ), "open(): $manifest: $!" );
}
return( $listfiles );
}
sub lock {
open( LOCK, '>', LOCKFILE ) || Snap->error( int( $! ), "open(): "
. LOCKFILE . ": Unable to open lock file" );
flock( LOCK, LOCK_EX|LOCK_NB ) || Snap->error( int( $! ), "flock(): "
. LOCKFILE . ": Unable to lock file" );
}
sub mkdirp{
( my $dir = shift ) =~ s/\/^//;
my $mode = shift;
( my $parent = $dir ) =~ s/\/[^\/]+$//;
if ( -d $dir ){
return;
}
mkdirp( $parent, $mode );
mkdir( $dir, $mode ) || Snap->error( int( $! ), "mkdir(): $dir: $!" );
}
### readconf() #############################################
#
# reads INI style files and builds a data structure with the
# parsed values.
#
# The config file can 'include' additional directories.
# Files in these directories will be parsed and added to
# the the data structure.
#
# Files ending in .conf will be parsed into the root of the
# data structure while .spt (snap package template) files
# will be stored in 'templates'
#
############################################################
sub readconf {
my $conffile = shift || CONFFILE;
my $data = shift || {};
my $section = '';
my $line = 0;
my $type;
if ( $conffile =~ /\.spt$/ ) {
$type = 'template';
}
elsif ( $conffile =~ /\.conf$/ ) {
$type = 'config';
}
open( my $fh, "<", $conffile ) || Snap->error( int( $! ),
"open(): $conffile: $!\n" );
while ( <$fh> ) {
chomp( $_ );
if ( $_ =~ /^\s*#/ || $_ =~ /^$/ ) {
next;
}
elsif ( $_ =~ /\s*include\s+(.*)$/ ) {
foreach my $dir ( split( ' ', $1 ) ) {
opendir( my $dh, $dir ) ||
Snap->error( int( $! ),
"opendir(): $dir: $!" );
while ( my $file = readdir( $dh ) ) {
if ( -f "$dir/$file" ) {
readconf( "$dir/$file", $data );
}
}
closedir( $dh ) || Snap->error( int( $! ),
"closedir(): $dir: $!" );
}
}
elsif ( $_ =~ /\s*\[(\S+)\]\s*/ ) {
$section = $1;
if ( $section eq 'sources' ) {
$data->{$section} = [];
}
}
elsif ( $section eq 'sources' ) {
push( @{$data->{$section}}, $_ );
}
elsif ( $section && $type eq 'template' ) {
if ( ! $data->{'templates'}{$section} ) {
$data->{'templates'}{$section} = [];
}
push( @{$data->{'templates'}{$section}}, $_ );
}
elsif ( $_ =~ /(\S+)\s*=\s*(.*)$/ ) {
$data->{$section}{$1} = $2;
}
}
close( $fh );
return( $data );
}
### setup() ################################################
#
# This should be called if any of the expected environment
# is found not to be present. This includes the directories
# in /var/lib/snap, /etc/snap.conf, and /etc/snap_version
#
############################################################
sub setup {
my $chkfails = 0;
my $target = 0;
my $snapdir = 0;
my $pkgdir = 0;
my $instdir = 0;
my $srcdir = 0;
my $yes = '';
if ( TARGET && ! -e TARGET ) {
$target++;
$chkfails++;
}
if ( ! -e SNAPDIR ) {
$snapdir++;
$chkfails++;
}
if ( ! -e PKGDIR ) {
$pkgdir++;
$chkfails++;
}
if ( ! -e INSTDIR ) {
$instdir++;
$chkfails++;
}
if ( ! -e SRCDIR ) {
$srcdir++;
$chkfails++;
}
if ( $target ) {
mkdir( TARGET, 0755 ) || Snap->error( int( $! ), "mkdir: $!" );
}
if ( $snapdir ) {
mkdirp( SNAPDIR, 0755 );
}
if ( $pkgdir ) {
mkdir( PKGDIR, 0755 ) || Snap->error( int( $! ),
"mkdir(): " . PKGDIR . ": $!" );
}
if ( $instdir ) {
mkdir( INSTDIR, 0755 ) || Snap->error( int( $! ),
"mkdir(): " . INSTDIR . ": $!" );
}
if ( $srcdir ) {
my $conf = readconf();
my $sources = Snap::Sources->new( $conf->{'sources'} );
mkdir( SRCDIR, 0755 ) || Snap->error( int( $! ),
"mkdir(): " . SRCDIR . ": $!" );
$sources->refresh();
}
}
sub sha {
my $file = shift;
my $digest = eval {
Digest::SHA->new( 1 )->addfile( $file );
} || Snap->error( -1, "sha(): $file: $!\n" );
return( $digest->hexdigest );
}
### sha256() ###############################################
#
# This sub returns a hex sha256 hash of a supplied file
#
############################################################
sub sha256 {
my $class = shift;
my $file = shift;
my $digest = eval {
Digest::SHA->new( 256 )->addfile( $file );
} || Snap->error( -1, "sha256(): $file: $!\n" );
return( $digest->hexdigest );
}
sub termsize {
my $row = 24;
my $col = 80;
my $sel = IO::Select->new();
my $stdout;
my $stderr;
my $stat;
my $pid;
eval {
$pid = open3( \*CHLDIN, \*CHLDOUT, \*CHLDERR,
"stty size -F /dev/tty" );
} || return( { row => $row, col => $col } );
close( CHLDIN );
$sel->add( *CHLDOUT, *CHLDERR );
while ( my @fhs = $sel->can_read ) {
foreach my $fh ( @fhs ) {
if ( eof( $fh ) ) {
$sel->remove( $fh );
next;
}
if ( fileno( $fh ) == fileno( *CHLDOUT ) ) {
$stdout .= <$fh>;
}
elsif ( fileno( $fh ) == fileno( *CHLDERR ) ) {
$stderr .= <$fh>;
}
}
}
close( CHLDOUT );
close( CHLDERR );
waitpid( $pid, 0 );
$stat = $? >> 8;
if ( $stdout =~ /(\d+)\s+(\d+)/ ) {
$row = $1;
$col = $2;
}
return( { row => $row, col => $col } );
}
sub unlock {
if ( -f LOCKFILE ) {
unlink( LOCKFILE ) || Snap->error( int( $! ), "unlink(): "
. LOCKFILE . ": Unable to remove lock file" );
}
}
### vercmp() ###############################################
#
# This subroutine was basically copied verbatim from the
# Sort::Versions module. It was modified slightly so that
# it more closely matched the aesthetics of the rest of
# the snap code. The following credits and license
# information were provided within the documentation of
# that module:
#
# Ed Avis <ed@membled.com> and Matt Johnson
# <mwj99@doc.ic.ac.uk> for recent releases; the original
# author is Kenneth J. Albanowski <kjahds@kjahds.com>.
# Thanks to Hack Kampbjørn and Slaven Rezic for patches
# and bug reports.
#
# Copyright (c) 1996, Kenneth J. Albanowski. All rights
# reserved. This program is free software; you can
# redistribute it and/or modify it under the same terms as
# Perl itself.
#
# Return values:
#
# -1: A < B
# 0: match
# 1: A > B
#
############################################################
sub vercmp {
my $class = shift;
my @A = ( $_[0] =~ /([-.]|\d+|[^-.\d]+)/g );
my @B = ( $_[1] =~ /([-.]|\d+|[^-.\d]+)/g );
my ( $A, $B );
while ( @A and @B ) {
$A = shift @A;
$B = shift @B;
if ( $A eq '-' and $B eq '-' ) {
next;
}
elsif ( $A eq '-' ) {
return -1;
}
elsif ( $B eq '-' ) {
return 1;
}
elsif ( $A eq '.' and $B eq '.' ) {
next;
}
elsif ( $A eq '.' ) {
return -1;
}
elsif ( $B eq '.' ) {
return 1;
}
elsif ( $A =~ /^\d+$/ and $B =~ /^\d+$/ ) {
if ( $A =~ /^0/ || $B =~ /^0/ ) {
return $A cmp $B if $A cmp $B;
}
else{
return $A <=> $B if $A <=> $B;
}
}
else {
$A = uc $A;
$B = uc $B;
return $A cmp $B if $A cmp $B;
}
}
@A <=> @B;
}
### virtfs() ###############################################
#
# Need to try and figure out a solution for user installs
#
# The user may not be able to mount the virt filesystems,
# so perhaps it could do some kind of linking instead?
#
# Also remember you might want to move chroot out of usher
# and into here! We could also try to do fakechroot as well
#
############################################################
sub virtfs {
my $command = shift;
my $virtfs = {
dev => {
fs => 'devtmpfs',
dev => 'none',
dir => TARGET . '/dev'
},
proc => {
fs => 'proc',
dev => 'none',
dir => TARGET . '/proc'
},
sys => {
fs => 'sysfs',
dev => 'none',
dir => TARGET .'/sys'
}
};
if ( $> || ! TARGET ) {
return;
}
foreach my $fs ( sort( keys( %$virtfs ) ) ) {
my $sel;
my $cmd;
my $stdout = '';
my $stderr = '';
my $stat = 0;
my $pid;
if ( ! -d $virtfs->{$fs}{'dir'} ) {
next;
}
if ( $command eq 'mount' ) {
if ( ismountpoint( $virtfs->{$fs}{'dir'} ) ) {
next;
}
$cmd = "mount -t $virtfs->{$fs}{'fs'}"
. " $virtfs->{$fs}{'dev'}"
. " $virtfs->{$fs}{'dir'}";
}
elsif ( $command eq 'umount' ) {
if ( ! ismountpoint( $virtfs->{$fs}{'dir'} ) ) {
next;
}
$cmd = "umount $virtfs->{$fs}{'dir'}";
}
else {
Snap->error( -1, "virtfs(): $command:"
. " not a valid command" );
}
eval {
$pid = open3( \*CHLDIN, \*CHLDOUT, \*CHLDERR, $cmd );
} || Snap->error( int( $! ), "open3(): $cmd: $!" );
close( CHLDIN );
$sel = IO::Select->new();
$sel->add( *CHLDOUT, *CHLDERR );
while ( my @fhs = $sel->can_read ) {
foreach my $fh ( @fhs ) {
if ( eof( $fh ) ) {
$sel->remove( $fh );
next;
}
if ( fileno( $fh ) == fileno( *CHLDOUT ) ) {
$stdout .= <$fh>;
}
elsif ( fileno( $fh ) == fileno( *CHLDERR ) ) {
$stderr .= <$fh>;
}
}
}
close( CHLDOUT );
close( CHLDERR );
waitpid( $pid, 0 );
$stat = $? >> 8;
if ( $stat ) {
Snap->error( $stat, "Failed $command for"
. " $virtfs->{$fs}{'dir'}: $stderr" );
}
}
foreach my $fs ( sort( keys( %$virtfs ) ) ) {
if ( ! ismountpoint( $virtfs->{$fs}{'dir'} ) ) {
return( 0 );
}
}
return( 1 );
}
1;