#!/bin/sh ############################################################################## # # # This entire file is toast, a program for installing and managing software. # # Copyright (C) 2003-2004 Jacques Frechet. # # Note that this file contains Version 2 of the GNU General Public License, # # which includes its own copyright notice. # # # # For more information on toast, including purpose, usage, licensing, and # # LACK OF ANY WARRANTY, visit http://www.toastball.net/toast/, run "toast" # # without arguments, or refer to the documentation at the end of this file. # # # ############################################################################## exec perl -x $0 ${1+"$@"} echo "Can't find perl in PATH; aborting." >&2 exit 1 ############################################################################## #!perl &the_correct_line_number_for_this_line_is(23); # perl thinks this is line 2... use 5; # oldest version that actually works might be 5.003; not sure.... # allow script to run even in the absence of strict.pm, etc. BEGIN { $^W = 1 } # use warnings BEGIN { $^H |= 0x602 } # use strict # warn if "use warnings" and "use strict" are not both in effect if($^V && %SIG) # test requires perl 5.6.0 with working %SIG (not microperl) { { my($ok); { local $SIG{'__WARN__'} = sub { die }; $ok = !eval('1 + "a"') } $ok || warn("use warnings"); } { eval('$foo = 1') && warn("use strict") } } ############################################################################## my($rcsid) = q$Id: toast,v 1.304 2004/02/27 00:15:37 zaphod Exp $; $rcsid =~ /^Id: (.+),v (\S+) (\S+ \S+) (\S+) Exp (\S+ )?$/ || die; my($myname, $myversion, $mytimestamp, $myauthor) = ($1, $2, $3, $4); $myversion .= "+" if $5; # if this version may contain changes not in RCS my($myurl) = "http://www.toastball.net/toast/"; my($mycopyright) = "Copyright (C) 2003-2004 Jacques Frechet"; my($genby) = "generated by $myname version $myversion [$myurl]"; $myname eq "toast" && $myauthor eq "zaphod" || die; # avoid accidents w/ CVS ############################################################################## BEGIN { my($linedelta, $filedelta); sub the_correct_line_number_for_this_line_is($) { my($realline) = @_; my($package, $file, $line) = caller(0); $linedelta = $realline - $line; $filedelta = $file; } sub trace(;$) { my($i, $out, $prev) = (shift || 1, "", ""); while(my($package, $file, $line) = caller($i++)) { $line += $linedelta if defined($linedelta) && $file eq $filedelta; $out .= ($prev eq $file ? "/" : $out ? "; $file: " : "$file: ") . $line; $prev = $file; } $out .= " (from #!perl line)" unless defined($linedelta); $out .= " v$myversion"; return $out; } } sub error(@) { die(join('', @_) || "assertion failed", "\n[" . trace . "]\n"); } sub warning(@) { warn(join('', @_) || "warning", "\n[" . trace . "]\n"); } ############################################################################## sub true() { 1 } sub false() { "" } sub emptytoundef(@) { map { defined($_) && $_ eq "" ? undef : $_ } @_; } sub undeftoempty(@) { map { defined($_) ? $_ : "" } @_; } sub firstdef(@) { return $_ foreach grep(defined($_), @_); undef; } sub samelist(\@\@) { my($x, $y) = @_; my($size) = scalar(@$x); return false if $size != scalar(@$y); return true unless $size > 0; for(0..$size-1) { my($vx, $vy) = ($$x[$_], $$y[$_]); my($dx, $dy) = map { defined($_) ? 1 : 0 } ($vx, $vy); return false if $dx != $dy || $dx & $vx ne $vy; } return true; } ############################################################################## sub max(@) { my($result); for(@_) { $result = $_ if !defined($result) || $result < $_; } $result; } sub min(@) { my($result); for(@_) { $result = $_ if !defined($result) || $result > $_; } $result; } ############################################################################## sub dirname($) { my($arg) = @_; $arg =~ s|/[^/]*$|| ? $arg : "."; } sub basename($) { my($arg) = @_; $arg =~ s|.*/||g; $arg; } sub stripext($) { my($arg) = @_; $arg =~ s/(\.tar)?(\.[a-z]\w*)?$//i; $arg; } sub stripquery($) { my($arg) = @_; $arg =~ s/\?.*$//; $arg; } sub path(@) { defined($_) || error("undefined path component in @_") foreach @_; local($_) = join("/", @_); m|//| && error("double slash in path(@_): $_"); m|/$| && error("final slash in path(@_): $_"); $_; } sub optpath(@) { path(grep { defined($_) && $_ ne "" } @_); } sub unpath($) { my($path) = @_; $path =~ s|^/|| || error; $path =~ m|^/| && error; $path =~ m|/$| && error; $path =~ m|//| && error; return split(/\//, $path); } ############################################################################## sub checkedeval($) { my($code) = @_; my($result) = eval($code); if($@) { local($_) = $code; s/\s+/ /g; error($@, $_); } $result; } ############################################################################## sub safeopen(*$$) { local(*HANDLE) = shift; my($mode, $file) = @_; my($escaped) = "$file\x00"; $escaped = "./$escaped" unless $escaped =~ m!^/!; open(HANDLE, "$mode$escaped") || error("open $file: $!"); binmode(HANDLE) || error("binmode $file: $!"); # perl 5.8.0 utf8 bug } sub whilefile(&$) { my($sub, $file) = @_; local(*FILE, $_); safeopen(*FILE, "<", $file); while(defined($_ = ) && &$sub($_)) { } close(FILE) || error("close $file for read: $!"); !defined($_); } sub readfile($) { my($file) = @_; my(@result); whilefile { push(@result, $_) } $file; @result; } ############################################################################## BEGIN { my($uid, $euid) = ($<, $>); sub superuser() { $uid == 0 || $euid == 0; } } ############################################################################## BEGIN { my(%optdefault) = ( "storedir" => (superuser || !$ENV{HOME}) ? "/toast" : "$ENV{HOME}/.toast", "armdir" => superuser ? "/usr/local" : "armed", "altarmdirs" => "", "username" => "toast", "postarmprog" => superuser ? "/sbin/ldconfig" : "", "editprog" => "", "defaultcmd" => "help", "httpproxy" => exists($ENV{http_proxy}) ? $ENV{http_proxy} : "", "ftpproxy" => exists($ENV{ftp_proxy}) ? $ENV{ftp_proxy} : "", "quiet" => "false", "autofind" => "true", "autochange" => "true", "autorename" => "true", "autoclean" => "true", "autopurge" => "false", "autoarm" => "true", "autodisarm" => "true", "autodemolish" => "true", "autoremove" => "false", "crossversion" => "false", "skipmismatched" => "true", "strictpreload" => "true", "useflock" => $^O =~ /win/i ? "false" : "true", "reconfigure" => "true", "fixliblinks" => "true", "stoponerror" => "true", "ignorecase" => "true", "showurls" => "true", "infodir" => "true", "protect" => "true", "relative" => "false", "debugrewrite" => "false", ); sub envopt($) { $ENV{uc("${myname}_$_[0]")}; } my(%optloaded); sub istrue($) { my($val) = @_; return $val && $val =~ /^(1|true|on|yes|enabled)$/i; } sub isboolean($) { my($val) = @_; return !$val || istrue($val) || $val =~ /^(false|off|no|disabled)$/i; } sub isopt($) { my($name) = @_; return exists($optdefault{$name}); } sub isboolopt($) { my($name) = @_; return false unless isopt($name); my($def) = $optdefault{$name}; return defined($def) && ($def eq "true" || $def eq "false"); } sub checkoptname($) { my($name) = @_; isopt($name) || error("no such option: $name"); } sub loadopt($) { my($name) = @_; error unless isopt($name); return $optloaded{$name} if %optloaded; $optloaded{1} = 1; return unless $ENV{HOME}; my($dotfile) = "$ENV{HOME}/.$myname/conf"; return unless -e($dotfile); whilefile { s/^\s+//; s/\s+$//; return true if $_ eq "" || /^\#/; /^([^\=]*?)\s*\=\s*(.*)$/ || error("$dotfile: line $.: missing \"=\""); my($name, $val) = ($1, $2); isopt($name) || error("$dotfile: line $.: unknown option name \"$name\""); !isboolopt($name) || isboolean($val) || error("$dotfile: line $.: illegal boolean value: \"$val\""); $optloaded{$name} = $val; true; } $dotfile; $optloaded{$name}; } my(%optcurrent); sub setopt($$) { my($name, $val) = @_; checkoptname($name); if(isboolopt($name)) { error("$name is a boolean option") unless isboolean($val); $val = istrue($val); } else { error("option $name is undefined") unless defined($val); if($name =~ /dir$/ && $val !~ m|^/|) { error("relative path not allowed for storedir: $val") if $name eq "storedir"; $val = path(&storedir, $val); } } $optcurrent{$name} = $val; } sub getopt($) { my($name) = @_; setopt($name, firstdef(envopt($name), loadopt($name), $optdefault{$name})) unless exists($optcurrent{$name}); return $optcurrent{$name}; } checkedeval("sub $_() { getopt('$_') }") foreach keys(%optdefault); } ############################################################################## sub pkgdir() { "pkg" } sub archivedir() { "archive" } sub editdir() { "edit" } sub urlfile() { "url" } sub armdirlink() { "armdir" } sub srcdir() { "src" } sub helperdir() { "helpers" } sub rootdir() { "root" } sub buildlog() { "build.log" } sub brokenlog() { "broken.log" } sub offsuffix() { ".off" } sub tmpsuffix() { ".tmp" } sub baksuffix() { ".orig" } ############################################################################## sub explain(@) { print("# @_\n") unless quiet; } sub announce(@) { print("@_\n") unless quiet; } ############################################################################## sub unbuffer(*) { local(*HANDLE) = shift; my($save) = select(HANDLE) || error; $| = 1; select($save) || error; } ############################################################################## sub whiledir(&$) { my($sub, $dir) = @_; local(*DIR, $_); opendir(DIR, $dir) || error("opendir $dir: $!"); readdir(DIR) eq "." || error; readdir(DIR) eq ".." || error; while(defined($_ = readdir(DIR)) && &$sub($_)) { } closedir(DIR); !defined($_); } sub fordir(&@) { my($sub, $dir) = @_; my($result) = true; whiledir { &$sub(@_) || !($result = false) } $dir; $result; } sub abswhiledir(&$) { my($sub, $dir) = @_; whiledir { &$sub($_ = path($dir, $_)) } $dir; } sub ls($) { my($dir) = @_; my(@result); whiledir { push(@result, $_) } $dir; @result; } sub optls($) { my($dir) = @_; return -e($dir) ? ls($dir) : (); } sub absls($) { my($dir) = @_; map { path($dir, $_) } ls($dir); } sub dfs($&&&) { my($base, $predir, $file, $postdir, $rel) = @_; defined($rel) || -e($base) || -l($base) || error("not found: $base"); local($_) = optpath($base, $rel); (-l || !-d) ? &$file($rel) : &$predir($rel) && (whiledir {&dfs($base, $predir, $file, $postdir, optpath($rel, $_))} $_) && &$postdir($rel); } ############################################################################## sub md(@) { announce("mkdir", @_); mkdir($_, 0777) || error("mkdir $_: $!") foreach @_; true; } sub optmd(@) { foreach(@_) { next if -d; announce("mkdir", $_); if(!mkdir($_, 0777)) { my($err) = $!; -d || error("mkdir $_: $err"); } } true; } ############################################################################## sub mv($$) { my($source, $dest) = @_; announce("mv", $source, $dest); rename($source, $dest) || error("mv $source $dest: $!"); } sub ln($$) { my($source, $target) = @_; announce("ln", "-s", $source, $target); symlink($source, $target) || error("ln -s $source $target: $!"); } sub optln($$) { my($source, $target) = @_; ln($source, $target) unless -e($target) || -l($target); } sub relln($$) { my($src, $target) = @_; ln($src =~ m|^/| ? findrelpath(dirname($target), $src) : $src, $target); } ############################################################################## sub safestat($) { my($file) = @_; my(@result) = stat($file); @result || error("stat $file: $!"); @result; } sub getmode($) { (safestat($_[0]))[2] & 07777 } sub chmodimpl($$@) { my($mode, $announce, @files) = @_; $mode &= 01777 & ~umask; my($txtmode) = sprintf("%lo", $mode); @files = grep { getmode($_) != $mode } @files; announce("chmod", $txtmode, @files) if @files && $announce; chmod($mode, $_) || error("chmod $txtmode $_: $!") for @files; true; } sub safechmod($@) { my($mode, @files) = @_; chmodimpl($mode, true, @files); } sub silentchmod($@) { my($mode, @files) = @_; chmodimpl($mode, false, @files); } sub writefilemode($$@) { my($name, $mode, @contents) = @_; local(*FILE); explain("creating $name"); safeopen(*FILE, ">", $name) || error("open $name for write: $!"); print FILE @contents; close(FILE) || error("close $name for write: $!"); safechmod($mode, $name) if $mode; } sub writefile($@) { my($name, @contents) = @_; writefilemode($name, false, @contents); } sub writescript($@) { my($name, @contents) = @_; writefilemode($name, 0777, @contents); } sub samefile($$) { my($a, $b) = @_; my($da, $ia) = safestat($a); my($db, $ib) = safestat($b); $da eq $db && $ia eq $ib; } sub optsamefile($$) { my($a, $b) = @_; my($da, $ia) = stat($a); return false unless defined($da); my($db, $ib) = stat($b); defined($db) && $da eq $db && $ia eq $ib; } sub patch(&$) { my($sub, $file) = @_; my(@contents) = readfile($file); my($patched) = false; local($_); for(@contents) { my($old) = $_; &$sub($_); $patched ||= $old ne $_; } if($patched) { my($mode) = getmode($file); mv($file, addbak($file)); writefilemode($file, $mode, @contents); } return $patched; } sub optpatch(&$) { my($sub, $file) = @_; return -e($file) ? &patch($sub, $file) : false; } ############################################################################## sub silentrm(@) { unlink || error("rm $_: $!") for @_; true; } sub rm(@) { announce("rm", "-f", @_) if @_; silentrm(@_); } sub silentrd(@) { rmdir || error("rmdir $_: $!") for @_; true; } sub rd(@) { announce("rmdir", @_) if @_; silentrd(@_); } sub rmall(@) { announce("rm", "-rf", @_) if @_; dfs ( $_, sub { -w($_) ? true : silentchmod(0777, $_) }, sub { silentrm($_) }, sub { silentrd($_) } ) for @_; true; } sub optrm(@) { rm(grep(-e || -l, @_)) } sub optrmall(@) { rmall(grep(-e || -l, @_)) } ############################################################################## sub nice($) { eval { setpriority(0, 0, min(getpriority(0, 0) + $_[0], 20)) }; } sub safeexec(@) { my(@prog) = @_; { local($^W) = false; # suppress misguided exec failure warning exec(@prog); } error("exec @prog: $!"); } sub runimpl(@) { my(@prog) = @_; announce(@prog); my($result); { local($^W) = false; # suppress misguided exec failure warning $result = system(@prog); } return "exec @prog: $!" unless defined($result) && $result != -1; my($code) = $? & 0xff; my($sig) = ($? >> 8) & 0xff; return "@prog failed (code $code, signal $sig)" if $?; return undef; } sub run(@) { my($msg) = runimpl(@_); error($msg) if defined($msg); return true; } sub optrun(@) { my($msg) = runimpl(@_); explain("warning: $msg; continuing") if defined($msg); return !defined($msg); } sub optcdrun($@) { my($dir, @prog) = @_; announce("(cd $dir; @prog)"); my($pid); if($pid = fork) # parent { waitpid($pid, 0); return !$?; } else # child { defined($pid) || error("fork: $!"); chdir($dir) || error("chdir $dir: $!"); safeexec(@prog); } } sub cdrun($@) { my($dir, @prog) = @_; optcdrun($dir, @prog) || error("@prog returned $?"); } sub shellescape(@) { my(@words) = @_; for(@words) { error unless defined($_); next unless m![^\w\+,\./:\@-]!; /'/ ? s!([^\w\+,\./:\@-])!\\$1!g : ($_ = "'$_'"); } return join(" ", @words); } sub openprog(*$@) { local(*HANDLE) = shift; my($prog, @args) = @_; announce($prog, @args); @args = shellescape(@args); { local($^W) = false; # suppress misguided exec failure warning if(!open(HANDLE, "$prog @args |")) { explain("failed to exec $prog: $!"); return false; } } binmode(HANDLE) || error("binmode: $!"); # perl 5.8.0 utf8 bug } ############################################################################## sub urlunescape($) { local($_) = @_; s/\%([0-9a-f]{2})/chr(hex($1))/gei; $_; } sub tcpconnect(*$$;$) { local(*HANDLE) = shift; my($host, $port, $ip) = @_; $ip = gethostbyname($host) || error("gethostbyname $host: $!") unless defined($ip); socket(HANDLE, 2, 1, 0) || error("$!"); binmode(HANDLE) || error("binmode: $!"); # perl 5.8.0 utf8 bug connect(HANDLE, pack("Sna4x8", 2, $port, $ip)) || error("connect $host:$port: $!"); unbuffer(HANDLE); return $ip; } sub openhttp(*$;$;$) { local(*HANDLE) = shift; my($url, $method, $proxy) = @_; $method ||= "GET"; $proxy = httpproxy unless defined($proxy); explain("fetching $url"); $url =~ m!^(\w+)://([\w\.]+)(:(\d+))?(/[\!-\~]*)?$! || error("bad url: $url"); my($proto, $host, $port, $path) = ($1, $2, $4 || 80, $5 || '/'); my($hdrhost) = $port == 80 ? $host : "$host:$port"; if($proxy) { $path = "$proto://$host:$port$path"; $proxy =~ m!^(\w+://)?([\w\.]+)(:(\d+))?([^:]+)(:(\d+))?/?$! || error("bad proxy URL: $proxy"); ($host, $port) = ($2, $4 || 8080); } my($uagent) = "$myname/$myversion ($^O; $myurl)"; my($request) = "$method $path HTTP/1.0\r\nHost: $hdrhost\r\n". "User-Agent: $uagent\r\nAccept: */*\r\n\r\n"; tcpconnect(*HANDLE, $host, $port); print HANDLE $request || error("write to $host:$port: $!"); } sub httphead($) { my($url) = @_; local(*HANDLE); openhttp(*HANDLE, $url, "HEAD"); my($result) = join('', ); close(HANDLE) || error; return $result; } sub openhttpurl(*$;$) { local(*HANDLE) = shift; my($url, $proxy) = @_; openhttp(*HANDLE, $url, undef, $proxy); local($_); $_ = ; s/\r?\n?$//; m|^HTTP/[\w\.]+ 200 | || error("$url: $_"); while() { last if /^\r?\n?$/ } return true; } sub ftpcmd(*$;$) { local(*CTRL, $_) = shift; my($cmd, $expected) = @_; print CTRL "$cmd\r\n" || error("error sending ftp command: $!") if $cmd; while() { s/[\r\n]+$//; if(/^(\d{3}) /) { my($response) = $1; error("ftp server: $_") if defined($expected) && $response != $expected; return $_; } } error("lost ftp control connection") unless $cmd =~ /QUIT/; } sub openftpurl(*$) { local(*HANDLE) = shift; my($url) = @_; return openhttpurl(*HANDLE, $url, ftpproxy) if ftpproxy; explain("fetching $url"); $url =~ m!^(\w+)://([\w\.]+)(:(\d+))?(/[\!-\~]*)?$! || error("bad url: $url"); my($proto, $host, $port, $path) = ($1, $2, $4 || 21, $5 || '/'); error("don't know how to fetch FTP directory listings") if $path =~ m!/$!; local(*CTRL, *DATA); my($ip) = tcpconnect(*CTRL, $host, $port); ftpcmd(*CTRL, undef, 220); ftpcmd(*CTRL, "USER anonymous", 230); ftpcmd(*CTRL, "TYPE I"); my($size) = ftpcmd(*CTRL, "SIZE $path", 213); $size =~ /^\d+\s+(\d+)/ or die("bad SIZE response: $size"); $size = $1; my($pasv) = ftpcmd(*CTRL, "PASV", 227); $pasv =~ /(\d+)\s*,\s*(\d+)\s*\)/ || error("bad PASV response: $pasv"); my($dataport) = $1*256 + $2; tcpconnect(*DATA, $host, $dataport, $ip); my($retr) = ftpcmd(*CTRL, "RETR $path"); $retr =~ /^(\d+) / && $1 <= 150 || error("ftp RETR error: $retr"); my($pid) = open(HANDLE, "-|"); error("fork: $!") unless defined($pid); if($pid) { close(CTRL) || error("close ftp control: $!"); close(DATA) || error("close ftp data: $!"); return true; } binmode(STDOUT) || error("binmode stdout: $!"); # perl 5.8.0 utf8 bug my($buf) = 0; while(sysread(DATA, $buf = "", min(8192, $size), 0)) { print($buf) || error("write stdout: $!"); $size -= length($buf); } error("lost ftp data connection") if $size; close(DATA) || error("close ftp data: $!"); ftpcmd(*CTRL, undef, 226); ftpcmd(*CTRL, "QUIT"); exit(0); error; } sub openfileurl(*$) { local(*HANDLE) = shift; my($url) = @_; $url =~ m!^file://([^/]+)(/.*)$! || error("bad url: $url"); my($host, $path) = ($1, $2); $host eq "localhost" || error("bad file-url hostname: $host"); my($source) = urlunescape($path); explain("reading $source"); safeopen(*HANDLE, "<", $source); } sub opensshurl(*$) { local(*HANDLE) = shift; my($url) = @_; $url =~ m!^ssh://([^/]+)(/.*)$! || error("bad url: $url"); my($userhost, $path) = ($1, $2); $userhost = urlunecape($userhost); $path = urlunescape($path); $userhost =~ /^\-/ && error("bad url: $url"); $path =~ /^\-/ && error("bad url: $url"); openprog(*HANDLE, "ssh", $userhost, "cat", $path); } sub openurl(*$) { local(*HANDLE) = shift; my($url) = @_; $url =~ /^(\w+):/ || error("bad url: $url"); my($proto) = lc($1); local($ENV{http_proxy}) = httpproxy if httpproxy; local($ENV{ftp_proxy}) = ftpproxy if ftpproxy; $proto eq "file" && openfileurl(*HANDLE, $url) || $proto eq "ssh" && opensshurl (*HANDLE, $url) || openprog(*HANDLE, "wget", "-O-", $url) || openprog(*HANDLE, "GET", $url) || $proto eq "http" && openhttpurl(*HANDLE, $url) || $proto eq "ftp" && openftpurl(*HANDLE, $url) || error("unable to download: $url"); } sub geturl($$) { my($url, $dest) = @_; local(*SOURCE, *DEST); explain("creating $dest"); safeopen(*DEST, ">", $dest); openurl(*SOURCE, $url); my($buf, $result); my(@dstat) = stat(DEST); my($bufsize) = $dstat[11] || 4096; while($result = read(SOURCE, $buf, $bufsize)) { (print DEST $buf) || error("write to $dest: $!"); } defined($result) || error("read from $url: $!"); close(DEST) || error("close $dest for write: $!"); close(SOURCE) || error("close $url for read: $!"); } sub linksfromstring($$) { my($html, $url) = @_; $url =~ m!^(((\w+:)//[^/]+)[^\?]*/)([^/\?](\?|$))?! || error("bad url: $url"); my($proto, $host, $parent) = ($3, $2, $1); my(@links); while($html =~ m!\b(href\s*\=\s*\"?|((http|ftp)://))([^\s\>\"]+)!ig) { my($link) = join("", undeftoempty($2, $4)); next if $link =~ /^mailto:/i; $link = $proto . $link if $link =~ m!^//!; $link = $host . $link if $link =~ m!^/!; $link = $parent . $link unless $link =~ m!^(\w+)://!; $link =~ s/\&/\&/g; push(@links, cleanurl($link)); } return @links; } sub linksfromstream(*$) { local(*HANDLE, $_) = shift; my($url) = @_; my(@links); while() { push(@links, linksfromstring($_, $url)); } return @links; } sub linksfromurl($) { my($url) = @_; local(*HANDLE); openurl(*HANDLE, $url); my(@links) = linksfromstream(*HANDLE, $url); close(HANDLE) || error("unable to fetch $url"); return @links; } ############################################################################## { my($pwd); sub pwd() { return $pwd if defined($pwd); my($env) = $ENV{PWD}; return $pwd = $env if defined($env) && -d($env) && samefile($env, "."); my($dir, $result) = (".", ""); for(1..1024) { my($parent) = path($dir, ".."); my($dd, $id) = safestat($dir); my($dp, $ip) = safestat($parent); return $pwd = "/$result" if $dd eq $dp && $id eq $ip; error("can't find working directory") if whiledir { my($d, $i) = safestat(path($parent, $_)); my($same) = $d eq $dd && $i eq $id; $result = optpath($_, $result) if $same; !$same; } $parent; $dir = $parent; } error("level>1024 in pwd"); } } sub abspath($) { my($path) = @_; return $path if $path =~ m!^/!; $path =~ s!^./!!; $path =~ s!/./!/!g; return path(pwd, $path); } sub findrelpath($$) { my($from, $to) = @_; # allow last component of $to to be missing or not a directory my(@append); if(!-d($to)) { push(@append, basename($to)); $to = dirname($to); } # walk from $to all the way to /, leaving a trail of bread crumbs my($dir, $lastdi, @names, %ditonameidx) = ($to, ""); for(1..1024) { my($d, $i) = safestat($dir); my($di) = "$d $i"; $ditonameidx{$di} = $#names unless exists($ditonameidx{$di}); last if $dir eq "/"; $dir = path($dir, ".."); last if whiledir # figure out how to get down one level from here { my($pd, $pi) = stat(path($dir, $_)); return true unless defined($pd) && "$pd $pi" eq $di; push(@names, $_); return false; } $dir; last if $di eq $lastdi; $lastdi = $di; } # walk up from $from towards / looking for bread crumbs ($dir, $lastdi) = ($from, ""); my(@result); for(1..1024) { my($d, $i) = safestat($dir); my($di) = "$d $i"; last if $di eq $lastdi; $lastdi = $di; if(exists($ditonameidx{$di})) { my($nameidx) = $ditonameidx{$di}; push(@result, reverse(@names[0..$nameidx])); push(@result, @append); return scalar(@result) == 0 ? "." : path(@result); } $dir = path($dir, ".."); push(@result, ".."); } error("no relative path from $from to $to"); } ############################################################################## sub validname($) { my($name) = @_; defined($name) && $name =~ /^[\w\x80-\xff][\w_ -\)\+-\-\x80-\xff]*[\w\x80-\xff]/; } sub validversion($) { my($version) = @_; defined($version) && $version =~ /^[\w\x80-\xff]([\w_ -\)\+-\-\x80-\xff\.]*[\w\x80-\xff])?/; } sub validbuild($) { my($build) = @_; defined($build) && /[1-9][0-9]*/; } ############################################################################## sub pkgpath(;$$$) { my($name, $version, $build) = @_; !defined($build) || ($build =~ /^[1-9]\d*$/) || error("bad build: $build"); optpath(storedir, pkgdir, $name, defined($version) && "v$version", $build); } sub addoff($) { my($path) = @_; error if offsuffix eq ""; return $path . offsuffix; } sub addtmp($) { my($path) = @_; error if tmpsuffix eq ""; return $path . tmpsuffix; } sub addbak($) { my($path) = @_; error if baksuffix eq ""; return $path . baksuffix; } ############################################################################## sub commonlen(@) { my(@sources) = @_; my($firstsource) = $sources[0] || error; my($i) = 0; while(true) { for(@sources) { return $i if length == $i || substr($_, $i, 1) ne substr($firstsource, $i, 1); } $i++; } } sub reverseall(@) { my(@result); push(@result, scalar reverse($_)) foreach @_; @result; } sub collapse(@) { my(@sources) = @_; my($firstsource) = $sources[0]; return $firstsource if scalar(@sources) == 1; my($frontlen) = commonlen(@sources); my($backlen) = commonlen(reverseall(@sources)); my($len) = length($firstsource); return $firstsource if $frontlen + $backlen >= $len || $frontlen + $backlen == 0; substr($firstsource, 0, $frontlen) . substr($firstsource, $len - $backlen); } sub sanitize($) { my($word) = @_; $word =~ s/\:/_/g; $word =~ s/^[\W_]+//; $word =~ s/[^a-z0-9\+]+$//i; # allow e.g. gtk+ $word =~ s/[\.-]?(source|src)$//i; # e.g. mozilla, libjpeg, minicom, XFree86 $word eq "" ? undef : $word; } sub guessnv(@) { my(@urls) = @_; my($base) = collapse(map(stripext(basename(stripquery($_))), @urls)); $base =~ /^([\w]+[\w\-]*[a-z]+)[-_]v?(\d[\w\.\+\-]+)$/i # Cryptix_src_3-1-1 || $base =~ /([^-]+)-(.*\d.+)/ # iputils-ss020124 || $base =~ /^(\D+[^a-z])v(\d.*)$/ # TinyMAZEv2.4a || $base =~ /^(\D+)(\d.*)$/ # fceu019linux || $base =~ /^(.*)()$/; # Xmerge my($name, $version) = ($1, $2); $name =~ s/\./_/g; # helps parse() distinguish filenames from pkg names return (sanitize($name), sanitize($version)); } ############################################################################## sub padfactor() { 40 } sub padsingle($) { my($arg) = @_; $arg = "" unless defined($arg); my($len) = length($arg); $len > padfactor ? $arg : (' ' x (padfactor - $len)) . $arg; } sub padnum($) { my($arg) = @_; my(@post) = $arg =~ s/-?([a-z]+)(\d+)$//i ? ($1, $2) : ("z" x padfactor, 0); my(@n) = split(/\./, $arg); join(' ', map(padsingle($_), @n[0..max(padfactor, $#n)], @post)); } sub cmpab() { my($na, $nb) = map { my($x) = $_; $x =~ s/\d+(\.\d+)*(-?(pre|rc|test)\d+)?/padnum($&)/gie; $x; } ($a, $b); my($lca, $lcb) = (lc($na), lc($nb)); $lca ne $lcb ? $lca cmp $lcb : $na ne $nb ? $na cmp $nb : $a cmp $b; } sub lastitem(@) { @_ ? $_[$#_] : undef; } sub mkcmdline(@) { join(' ', map { my($a) = $_; $a =~ s/[^\w_\-\.\/]/\\$&/g; $a; } @_); } ############################################################################## sub getuidgid() { my($username) = username; my($name, $passwd, $uid, $gid) = getpwnam($username); error("getpwnam $username: $!") unless defined($name); return ($uid, $gid); } sub dropprivs() { return unless superuser; my($username) = username; explain("running as user $username"); my($uid, $gid) = getuidgid; $uid || error("refusing to run as root"); $( = $gid; $) = "$gid $gid"; ($<, $>) = ($uid, $uid); $> == $< || error("real and effective UIDs do not match"); $> == $uid || error("uid is not set correctly"); } ############################################################################## sub findperl() { if($^X !~ m!/!) # 5.8.2 doesn't need all this, but 5.6.1 seems to { for(split(/:/, $ENV{"PATH"})) { my($path) = path($_, $^X); return $path if -x($path); } } return abspath($^X); } ############################################################################## sub yes() { explain("forking yes subprocess"); my($pid) = open(STDIN, "-|"); error("fork: $!") unless defined($pid); return if $pid; print("\n") || exit(0) foreach 1..9999; error("yes count exceeded"); } sub showprebuildinfo($$$) { my($name, $version, $build) = @_; explain("$myname $myversion building " . pkgname($name, $version, $build)); explain("$^X $0 $] $^O"); optrun("uname", "-a"); optrun(findperl, "-V"); optrun("printenv"); my(@urls) = pkgurls($name, $version); explain("no urls") unless @urls; explain("url: $_") for @urls; my(@archives) = absls(path(pkgpath($name, $version), archivedir)); explain("no archives") unless @archives; optrun("md5sum", @archives) if @archives; true; } sub gettimes() { (time, times) } sub showtimedeltas(@) { my($swall, $suser, $ssys, $scuser, $scsys) = @_; my($ewall, $euser, $esys, $ecuser, $ecsys) = gettimes; my($text) = ""; if(defined($suser)) { $text .= ($euser - $suser + $ecuser - $scuser) . "s user "; $text .= ($esys - $ssys + $ecsys - $scsys ) . "s system "; } $text .= ($ewall - $swall) . "s total"; explain($text); } ############################################################################## # patch files may have leading garbage, so it can help for this to be largish: sub magicbufsize { 1024 } sub magicstring($) { local($_) = @_; return ".Z" if /^\x1f\x9d/; return ".gz" if /^\x1f\x8b/; return ".bz2" if /^BZ/; return ".zip" if /^PK\x03\x04/; return ".rpm" if /^\xed\xab\xee\xdb/; return ".deb" if /^\!\\n/; return ".cpio" if /^07070[a-f\d]{30}/i; return ".tar" if /^[^\x00]+(..)?\x00{5,}[\x00\s\d]{30}/ || /\x00ustar[ \x00]/; return ".shar" if m:^\#!/bin/sh\n\# This is a shell archive:; return ".patch" if /^(diff |\*\*\* |Only in |Common subdirectories: |--- )/m; return ""; } sub magicfile($) { my($file) = $_; local(*FILE); safeopen(*FILE, "<", $file); my($buf); defined(read(FILE, $buf, magicbufsize)) || error("read $file: $!"); my($type) = magicstring($buf); if(!$type && length($buf) >= magicbufsize) { seek(FILE, -22, 2) || error("seek $file: $!"); defined(read(FILE, $buf = "", 22)) || error("read $file: $!"); $type = ".zip" if $buf =~ /^PK\x05\x06/; # end-of-directory signature } close(FILE) || error("close $file for read: $!"); return $type; } sub readstdin($) { my($len) = @_; my($buf) = ""; while($len > 0) { my($result) = sysread(STDIN, $buf, $len, length($buf)); defined($result) || error("read stdin: $!"); last unless $result; $len -= $result; } $buf; } sub skipstdin($) { my($len) = @_; while($len > 0) { my($chunk) = min($len, 8192); error("unexpected eof") unless length(readstdin($chunk)) == $chunk; $len -= $chunk; } return true; } sub forkstdin() { my($pid) = open(STDIN, "-|"); defined($pid) || error("fork stdin: $!"); binmode(STDIN) || error("binmode stdin: $!"); # perl 5.8.0 utf8 bug return $pid; } sub dumpstdin($) { my($buf) = @_; binmode(STDOUT) || error("binmode stdout: $!"); # perl 5.8.0 utf8 bug print($buf); print($buf) while sysread(STDIN, $buf = "", 8192, 0); exit(0); } sub extractstdin($); sub autoextractstdin(;$) { my($buf) = @_; $buf = readstdin(magicbufsize) unless defined($buf); my($type) = magicstring($buf); error("unknown file type: " . unpack("H*", $buf)) unless $type; forkstdin ? extractstdin($type) : dumpstdin($buf); } sub applypatchfromstdin() { my($subdir); my($ok) = whiledir { !defined($subdir) && ($subdir = $_) } "."; safeexec(qw[patch -p1 -d], $subdir) if $ok && $subdir; safeexec(qw[patch -p0]); } sub rpmextractstdin() { my($lead) = readstdin(96); $lead =~ /^\xed\xab\xee\xdb[\x03\x04]/ || error("not rpm v3 or v4"); my($pad) = 0; my($hdr); while(magicstring($hdr = readstdin($pad + 16)) !~ /^\..z/i) { error("short rpm: " . unpack("H*", $hdr)) unless length($hdr) == $pad + 16; my($magic, $zero, $sections, $bytes) = unpack("x$pad N4", $hdr); $magic == 0x8eade801 || error(sprintf("bad rpm header: %08x", $magic)); skipstdin($bytes + 16*$sections); $pad = (8 - $bytes%8) % 8; } autoextractstdin($hdr); error; } sub debextractstdin() { my($magic) = readstdin(8); error("bad deb magic: " . unpack("H*", $magic)) unless $magic eq "!\n"; for(;;) { my($hdr) = readstdin(60); error("bad deb hdr: " . unpack("H*", $hdr)) unless $hdr =~ / (\d+) +\`$/; my($len) = $1; skipstdin(1) if $hdr =~ s/^\n//; autoextractstdin if $hdr =~ /^data\./; # autoextractstdin doesn't return skipstdin($len); } error; } sub extractstdin($) { my($type) = @_; safeexec("tar", "xf", "-") if $type eq ".tar"; safeexec("cpio", "-di") if $type eq ".cpio"; safeexec("/bin/sh") if $type eq ".shar"; applypatchfromstdin if $type eq ".patch"; rpmextractstdin if $type eq ".rpm"; debextractstdin if $type eq ".deb"; if($type =~ /^\.(Z|gz|bz2)$/) { my($prog) = $type eq ".bz2" ? "bzip2" : "gzip"; open(STDIN, "$prog -cd |") || error("$prog: $!"); binmode(STDIN) || error("binmode stdin: $!"); # perl 5.8.0 utf8 bug autoextractstdin; error; } error("unable to handle $type data in this context") if $type; error("unknown file type"); } sub extractfile($$) { my($infile, $outdir) = @_; explain("extracting $infile"); my($type) = magicfile($infile); my($pid) = fork; error("fork: $!") unless defined($pid); if($pid) { waitpid($pid, 0); error("extract subprocess returned $?") if $? && !($? == 256 && $type eq ".zip"); } else { chdir($outdir) || error("chdir $outdir: $!"); safeexec("unzip", "-qo", $infile) if $type eq ".zip"; safeopen(*STDIN, "<", $infile); extractstdin($type); error; } } sub extractname($) { my($file) = @_; my($arg) = local($_) = $file; s/\.tgz$/.tar.gz/i; my($cmd, $ok) = ("", false); ($cmd, $arg) = ($cmd . "gzip -cd $arg | ", "-") if s/\.gz$//i; ($cmd, $arg) = ($cmd . "bzip2 -cd $arg | ", "-") if s/\.bz2$//i; ($cmd, $ok) = ($cmd . "tar tf $arg | ", true) if s/\.tar$//i; ($cmd, $ok) = ("unzip -Z1 $arg | ", true) if $arg eq $_ && s/\.zip$//i; return undef unless $ok; $cmd .= "head -1"; explain($cmd); local(*SAVE); open(SAVE, "<&STDIN") || error("save stdin: $!"); safeopen(*STDIN, "<", $file); my($result) = `$cmd`; open(STDIN, "<&SAVE") || error("restore stdin: $!"); chomp $result; $result =~ s|/+$||; emptytoundef($result); } ############################################################################## sub allnames(;$) { @_ && defined($_[0]) ? @_ : sort cmpab optls(pkgpath); } sub allversions($;$) { my($name) = shift; @_ && defined($_[0]) ? @_ : sort cmpab grep { s/^v// } ls(pkgpath($name)); } sub allbuilds($$;$) { my($name, $version) = (shift, shift); @_ && defined $_[0] ? @_ : sort { $a<=>$b } grep { /^[1-9]\d*$/ } ls(pkgpath($name, $version)); } ############################################################################## sub isname($) { my($name) = @_; defined($name) && -d(pkgpath($name)); } sub isversion($$) { my($name, $version) = @_; defined($name) && defined($version) && -d(pkgpath($name, $version)); } sub isbuild($$$) { my($name, $version, $build) = @_; defined($name) && defined($version) && defined($build) && -d(pkgpath($name, $version, $build)); } ############################################################################## sub latestversion($;$) { my($name, $version) = @_; lastitem(allversions($name, $version)); } sub latestbuilt($$;$) { my($name, $version, $build) = @_; defined($name) || error; defined($version) || error; return undef unless isversion($name, $version); for $build (reverse(allbuilds($name, $version, $build))) { return $build if isbuiltmatch($name, $version, $build); } return undef; } ############################################################################## sub whilebuild(&@) { my($sub, $name, $version, $build, @urls) = @_; @urls && error; for $name (allnames($name)) { for $version (allversions($name, $version)) { for $build (allbuilds($name, $version, $build)) { return false unless &$sub($name, $version, $build); } } } return true; } ############################################################################## sub isadded($$) { my($name, $version) = @_; return -d(pkgpath($name, $version)); } sub isstored($$) { my($name, $version) = @_; return -d(path(pkgpath($name, $version), archivedir)); } sub isbuilt(@) { my($name, $version, $build) = @_; return isadded($name, $version) && !whilebuild { my($name, $version, $build) = @_; return !-f(path(pkgpath($name, $version, $build), buildlog)); } @_; } sub isbroken($$$) { my($name, $version, $build) = @_; return -f(path(pkgpath($name, $version, $build), brokenlog)); } sub isclean($$$) { my($name, $version, $build) = @_; return !-d(path(pkgpath($name, $version, $build), srcdir)); } sub isbuildarmedin($$$$) { my($armdir, $name, $version, $build) = @_; $build || error; my($rootdir) = path(pkgpath($name, $version, $build), rootdir); return -d($rootdir) && !dfs ( $rootdir, sub { true }, sub { my($rel) = @_; my($armfile) = path($armdir, $rel); while(-e($armfile) || -l($armfile)) { return false if optsamefile($_, $armfile); $armfile = addoff($armfile); } return true; }, sub { true } ); } sub allarmdirs() { my(@armdirs, %seendi); for (armdir, split(/:/, altarmdirs)) { my($armdir) = m!^/! ? $_ : path(storedir, $_); my($device, $inode) = stat($armdir); next unless defined($device); next unless -d(_); my($di) = "$device $inode"; next if exists($seendi{$di}); $seendi{$di} = true; push(@armdirs, $armdir); } return @armdirs; } sub isarmed(@) { return !whilebuild { my($name, $version, $build) = @_; for (allarmdirs) { return false if isbuildarmedin($_, $name, $version, $build); } return true; } @_; } sub isarmedmatch(@) { return isarmed(@_) if !skipmismatched; return !whilebuild { my($name, $version, $build) = @_; return !isbuildarmedin(armdir, $name, $version, $build); }; } sub ismismatched($$$) { my($name, $version, $build) = @_; $build || error; my($armdirlink) = path(pkgpath($name, $version, $build), armdirlink); my($armdirisdir, $linkisdir) = (-d(armdir), -d($armdirlink)); return !samefile($armdirlink, armdir) if $armdirisdir && $linkisdir; return true if !$armdirisdir && $linkisdir; my($target) = readlink($armdirlink); return false unless defined($target); return $target ne armdir; } sub isbuiltmatch(@) { return isbuilt(@_) if !skipmismatched; my($name, $version, $build) = @_; return !whilebuild { my($name, $version, $build) = @_; return !isbuilt(@_) || ismismatched($name, $version, $build); } @_; } ############################################################################## sub lookslikepkgurl($;$;$) { my($url, $name, $version) = @_; return false unless $url =~ m!^(http|ftp)://!; return false if $url =~ m/\#/; my($noquery) = stripquery($url); return false unless $noquery =~ m!\.\w+$!; return false if $noquery =~ m!\.(html?|php)$!i; return true unless defined($name); my($basename) = basename($noquery); return false unless $basename =~ /\Q$name\E/i; return true unless defined($version); return false unless $basename =~ /\Q$version\E/i; return true; } sub findnewpkg($$) { my($name, $version) = @_; defined($name) || error; $name =~ /^[\w-]+$/ || error("invalid package name: $name"); my($lcname) = lc($name); my($url); if($lcname eq $myname) { $url = $myurl; } else { local(*XML, $_); my($sitename) = "freshmeat.net"; openurl(*XML, "http://freshmeat.net/projects-xml/$lcname/$lcname.xml"); my($notfound, %fmurl); while() { $notfound = /^Error: project not found/i ? 1 : 0 if !defined($notfound); $fmurl{$1} = $2 while m!([^<]+)= 0) { $version = "unknown" . ($max + 1); $verdir = pkgpath($name, $version); } $errmsg = mkdir($verdir, 0777) ? false : $!; error("mkdir $verdir: $errmsg") if $errmsg && !-d($verdir); } announce("mkdir", $verdir); } setpkgurls($name, $version, @urls); ($name, $version, @urls); } ############################################################################## sub smartgeturl($$) { my($url, $dir) = @_; my(%visited); for(1..5) { my($basename) = basename(stripquery($url)); $basename = "index" if $basename eq ""; my($file) = path($dir, $basename); geturl($url, $file); $visited{$url} = 1; local(*FILE); safeopen(*FILE, "<", $file); my($header); read(FILE, $header, 128) || error("read $file: $!"); my($redir); if($header =~ /^\<.*\bHTML\b/i) { seek(FILE, 0, 0) || error("rewind $file: $!"); my(@links) = reverse(sort cmpab linksfromstream(*FILE, $url)); my(@goodlinks) = grep(/\.tar\./, @links); @goodlinks = grep(!/\.(s?html?|php)$/i, @links) unless @goodlinks; @links = @goodlinks if @goodlinks; for (@links) { $redir = $_ if !$redir && basename(stripquery($_)) eq $basename && !$visited{$_}; } my($ext); for $ext (qw[.tar.bz2 .tar.gz .tgz .zip .deb .rpm]) { for (@links) { $redir = $_ if !$redir && stripquery($_) =~ /\Q$ext\E$/i && !$visited{$_}; } } } close(FILE) || error("close $file: $!"); return $url unless $redir; rm($file); $url = $redir; error if $visited{$url}; } error("too many links: $url"); } sub renamepkg($$$$) { my($oldname, $oldversion, $newname, $newversion) = @_; my($oldnamedir) = pkgpath($oldname); my($newnamedir) = pkgpath($newname); my($oldverdir) = pkgpath($oldname, $oldversion); my($newverdir) = pkgpath($newname, $newversion); optmd($newnamedir); mv($oldverdir, $newverdir); rmdir($oldnamedir) && announce("rmdir", $oldnamedir); return ($newname, $newversion); } sub autorenamepkg($$@) { my($name, $version, @urls) = @_; if(@urls) { my($newname, $newversion) = guessnv(@urls); return renamepkg($name, $version, $newname, $newversion) if defined($newname) && defined($newversion); } my($verdir) = pkgpath($name, $version); my($archivedir) = path($verdir, archivedir); local($_); for(absls($archivedir)) { my($extractname) = extractname($_); next unless defined($extractname); my($newname, $newversion) = guessnv($extractname); return renamepkg($name, $version, $newname, $newversion) if defined($newname) && defined($newversion); } ($name, $version); } sub get(@) { my($name, $version, $build, @urls) = @_; my($autorename) = @urls && !defined($version) && autorename; ($name, $version, @urls) = add(@_) if @urls || !isadded($name, $version); $build && error; defined($name) || error; defined($version) || error; @urls = pkgurls($name, $version) unless @urls; my($verdir) = pkgpath($name, $version); my($realdir) = path($verdir, archivedir); my($tempdir) = addtmp($realdir); optmd($tempdir); my($changed) = false; for(@urls) { my($newurl) = smartgeturl($_, $tempdir); if($newurl ne $_ && autochange) { $_ = $newurl; $changed = true; } } setpkgurls($name, $version, @urls) if $changed; mv($tempdir, $realdir); ($name, $version) = autorenamepkg($name, $version, $changed ? @urls : ()) if $autorename; ($name, $version, @urls); } ############################################################################## sub exprecedence($) { local($_) = @_; /\.(patch|diff)\b/i; } sub excmp($$) { my($a, $b) = @_; exprecedence($a) - exprecedence($b) || $a cmp $b; } sub extract($$) { my($indir, $outdir) = @_; my(@infiles) = absls($indir); @infiles || error("$indir is empty"); extractfile($_, $outdir) foreach sort { excmp($a, $b) } @infiles; } sub makedir($) { my($dir) = @_; my($subdir); my($ok) = abswhiledir { -d && !defined($subdir) && ($subdir = $_) } $dir; error("empty directory: $dir") if $ok && !defined($subdir); return $ok ? &makedir($subdir) : $dir; } sub quote($) { local($_) = undeftoempty(@_); s/\\/\\\\/g; s/'/\\'/g; "'$_'"; } sub cquote($) { local($_) = @_; s/\\/\\\\/g; s/"/\\"/g; "\"$_\""; } sub helpstub($$) { my($helperdir, $cmd) = @_; my($path) = quote($ENV{PATH}); writescript(path($helperdir, $cmd), "#!/bin/sh\nPATH=$path\nexec $cmd \"\$\@\"\n"); } sub helpnop($$) { my($helperdir, $cmd) = @_; helpstub($helperdir, $cmd); writescript(path($helperdir, "$cmd.helper"), "#!/bin/sh\ntrue\n"); } sub helprewrite($$$$$) { my($srcdir, $rootdir, $helperdir, $cmd, $force) = @_; helpstub($helperdir, $cmd); my($perl) = findperl; error("can't find perl: $perl") unless -x($perl); writescript(path($helperdir, "$cmd.helper"), "#!$perl\n# $genby\n", q[ $cmd = ], quote($cmd), q[; $srcdir = ], quote($srcdir), q[; $rootdir = ], quote($rootdir), q[; $armdir = ], quote(armdir), q[; $path = ], quote($ENV{PATH}), q[; $preload = ], quote($ENV{LD_PRELOAD}), q[; $myname = ], quote($myname), q[; # cleanse environment $ENV{PATH} = $path; if($preload eq "") { delete($ENV{LD_PRELOAD}); } else { $ENV{LD_PRELOAD} = $preload; } # save old arguments @oldargs = @ARGV; # compute new arguments my($dashp) = $cmd eq "mkdir"; # force mkdir -p while(@ARGV) { $_ = shift(@ARGV); $dashp &&= $_ ne "-p"; if($cmd eq "install" && /^-(o|-owner|g|-group)$/) { $rewritten = 1; shift(@ARGV); } else { if(m|^/| && !m!^(\Q$srcdir\E|\Q$rootdir\E)($|/)!) { $rewritten = 1; s!^\Q$armdir\E($|/)!$1!; my($dir, $elem) = ""; for $component (split(/\//, $_)) { $dir ne "" && mkdir("$rootdir$dir", 0777) && print("$myname: created directory $rootdir$dir\n"); $dir .= "/$component"; } $_ = "$rootdir$_"; } push(@newargs, $_); } } if($dashp) { $rewritten = 1; unshift(@newargs, "-p"); } # print and execute real command print("$myname: rewriting $cmd @oldargs -> $cmd @newargs\n") if $rewritten; ], $force ? q[ # return success, even if command returns failure system($cmd, @newargs) == -1 && die("system $cmd: $!"); exit(0); ] : q[ # allow command to fail exec($cmd, @newargs); die("exec $cmd: $!"); ]); } sub helplib($$$) { my($srcdir, $rootdir, $helperdir) = @_; my($defdebug) = "#define DEBUG stderr"; $defdebug = "/* $defdebug */" unless debugrewrite; my($code) = qq[/* $genby */ #define _GNU_SOURCE #include #include #include #include #ifdef __CYGWIN__ #ifndef RTLD_NEXT #define RTLD_NEXT ((void *) -1L) #endif #endif #ifndef DEBUG $defdebug #endif struct timeval; /* to match the utimes() prototype that SunOS pulls in... */ #ifdef DEBUG #define debug(fmt, args...) do { fprintf(DEBUG, "$myname: " fmt "\\n", ## args); fflush(DEBUG); } while(0) #else #define debug(fmt, args...) ((void)0) #endif #define BUFSIZE 4096 static const char *srcdir = ] . cquote($srcdir) . qq[; static const char *rootdir = ] . cquote($rootdir) . qq[; static const char *helperdir = ] . cquote($helperdir) . qq[; static const char *armdir = ] . cquote(armdir) . qq[; static int startswith(const char *s, const char *prefix) { int result = strncmp(s, prefix, strlen(prefix)) == 0; /* debug("startswith(%s, %s) = %d", s, prefix, result); */ return result; } static const char *check_strip_armdir(const char *pathname) { if(!pathname) { debug("NULL pathname"); return 0; } else if(pathname[0] != '/') { debug("relative pathname: %s", pathname); return 0; } else if(startswith(pathname, srcdir)) { debug("pathname in srcdir: %s", pathname); return 0; } else if(startswith(pathname, rootdir)) { debug("pathname in rootdir: %s", pathname); return 0; } else if(startswith(pathname, helperdir)) { debug("pathname in helperdir: %s", pathname); return 0; } else { if(startswith(pathname, armdir)) { debug("pathname in armdir: %s", pathname); pathname += strlen(armdir); debug("stripping armdir: %s", pathname); } if(BUFSIZE < strlen(rootdir) + strlen(pathname) + 1) { debug("pathname is too long: %s", pathname); return 0; } else { debug("pathname is rewritable: %s", pathname); return pathname; } } } static int real_mkdir(const char *pathname, int mode); static void mkparents(const char *pathname) { const char *stripped = check_strip_armdir(pathname); if(!stripped) { debug("not creating parents for %s", pathname); } else { int saved_errno = errno; char buf[BUFSIZE]; char *p = buf + strlen(rootdir); int done = 0; memset(buf, 0, sizeof(buf)); strcpy(buf, rootdir); while(!done) { do { *(p++) = *(stripped++); done = done || !*stripped; } while(!done && *stripped != '/'); if(!done) { int ret = real_mkdir(buf, 0755) != 0; if(ret == -1) { debug("can't create parent directory %s", buf); } else { debug("created parent directory %s", buf); } } } errno = saved_errno; } } static const char *rewrite(const char *pathname) { static char buffers[2][BUFSIZE]; static int whichbuf = 0; const char *stripped = check_strip_armdir(pathname); if(!stripped) { debug("not rewriting pathname %s", pathname); return pathname; } else { char *buffer = buffers[whichbuf]; whichbuf ^= 1; strcpy(buffer, rootdir); strcat(buffer, stripped); debug("rewriting %s -> %s", pathname, buffer); return buffer; } } ]; my(@decls) = split(/;/, q[ wrap FILE *fopen(const char *pathname, const char *mode); wrap FILE *freopen(const char *pathname, const char *mode, FILE *file); wrap void *opendir(const char *pathname); wrap void *dlopen(const char *pathname, int flag); wrap int creat(const char *pathname, int flags); wrap int open(const char *pathname, int flags, int mode); wrap int access(const char *pathname, int flags); wrap int chdir(const char *pathname); wrap int xstat(int version, const char *pathname, void *foo); wrap int lxstat(int version, const char *pathname, void *foo); wrap int readlink(const char *pathname, char *buf, int size); wrap int link(const char *pathname1, const char *pathname2); wrap int symlink(const char *pathname1, const char *pathname2); wrap0 int mkdir(const char *pathname, int mode); wrap int rmdir(const char *pathname); wrap int chmod(const char *pathname, int mode); wrap0 int rename(const char *pathname1, const char *pathname2); wrap0 int unlink(const char *pathname); wrap int utime(const char *pathname, void *foo); wrap int utimes(const char *pathname, const struct timeval *tvp); suppress int chown(const char *pathname, int user, int group); suppress int lchown(const char *pathname, int user, int group); suppress int fchown(int fd, int user, int group); ]); for(@decls) { s/\s+//; next unless $_; /^(wrap0?|suppress) (int |\w+ \*)(\w+)\((.*)\)$/ || error; my($iswrap, $rettype, $basename, $proto) = ($1 ne "suppress", $2, $3, $4); # wrapped dlopen() always fails with "Service not available" in FreeBSD (?) next if $basename eq "dlopen" && $^O eq "freebsd"; my($iswrap0) = $1 eq "wrap0"; my($retfailed, $retfmt) = $rettype =~ /\*/ ? (0, '%p') : (-1, '%d'); my(@alist) = map { /\w+$/ ? $& : error } split(/, /, $proto); my($args) = join(", ", @alist); my($newargs) = join(", ", map { /pathname/ ? "rewrite($_)" : $_ } @alist); my($allrewritable) = join(" && ", map("check_strip_armdir($_)", grep(/pathname/, @alist))); my($destarg) = $proto =~ /pathname2/ ? "pathname2" : "pathname"; my($nameprefix, $namesuffix); for $nameprefix ("", "_", "__") { for $namesuffix ("", "64") { my($name) = "$nameprefix$basename$namesuffix"; $code .= $iswrap ? qq[ static $rettype(*next_$name)($proto) = 0; static ${rettype}real_$name($proto) { if(!next_$name) { next_$name = dlsym(RTLD_NEXT, "$name"); } return next_$name($args); } $rettype$name($proto) { ${rettype}result; debug("rewriting $name"); mkparents($destarg); result = real_$name($newargs); debug("rewritten $name returned $retfmt", result); if(result == $retfailed) { debug("retrying $name with original args"); result = real_$name($args); debug("original $name returned $retfmt", result); } ] . ($iswrap0 ? qq[ if(result == $retfailed && $allrewritable) { debug("suppressing $name"); result = 0; errno = 0; } ] : "") . qq[ return result; } ] : qq[ $rettype$name($proto) { debug("suppressing $name"); return 0; } ]; } } } my(@socc) = qw[gcc -shared -Wall]; push(@socc, qw[-Werror]) if strictpreload; my(@sol) = (); push(@sol, qw[-ldl]) if $^O =~ /linux/i; writefile(path($helperdir, "dummy.c"), "void ____toast_dummy_4u38291(){}\n"); writefile(path($helperdir, "rewrite.c"), $code); return path($helperdir, "rewrite.so") if optcdrun($helperdir, @socc, qw[dummy.c -o rewrite.so]) && optcdrun($helperdir, @socc, qw[rewrite.c -o rewrite.so.helper], @sol); error("unable to compile with strictpreload enabled") if strictpreload; explain("unable to compile; strictpreload disabled, continuing"); return undef; } sub helphome($$$) { my($srcdir, $rootdir, $helperdir) = @_; my($makehome) = path($helperdir, "home"); my($installhome) = path($helperdir, "home.helper"); md($makehome, $installhome); my($makedir) = makedir($srcdir); my($scriptdir) = path($installhome, "bin"); my($script) = path($scriptdir, "installkernel"); my($kbase) = "vmlinuz"; my($mbase) = "System.map"; my($kernel) = path($rootdir, "boot", $kbase); my($map) = path($rootdir, "boot", $mbase); md($scriptdir); writescript($script, qq[#!/bin/sh # script to be run by Linux kernel Makefile install target # $genby set -e set -x cat \$2 > $kernel cp \$3 $map ln -s $kbase $kernel-\$1 ln -s $mbase $map-\$1 (cd $makedir; tar cf - include/linux include/asm/.) | (cd $rootdir; tar xf -) ]); return $makehome; } sub inithelpers($$$) { my($srcdir, $rootdir, $helperdir) = @_; my(%env) = %ENV; md($helperdir); helprewrite($srcdir, $rootdir, $helperdir, $_, false) foreach (qw[mkdir rmdir touch cp rm ln install chmod test ranlib gzip], "["); helprewrite($srcdir, $rootdir, $helperdir, $_, true) foreach (qw[mv]); helpnop($helperdir, $_) foreach (qw[chown ldconfig install-info]); $env{PATH} = "$helperdir:$env{PATH}"; my($preload) = helplib($srcdir, $rootdir, $helperdir); if($preload) { if(exists($env{LD_PRELOAD}) && length($env{LD_PRELOAD})) { $env{LD_PRELOAD} .= " $preload"; } else { $env{LD_PRELOAD} = $preload; } } $env{HOME} = helphome($srcdir, $rootdir, $helperdir); %env; } sub armhelpers($) { my($helperdir) = @_; my(@files) = absls($helperdir); /\.helper$/ ? mv($_, $`) : mv($_, "$_.stub") foreach sort(@files); } sub makefile($) { my($dir) = @_; for(qw[GNUmakefile makefile Makefile]) { my($file) = path($dir, $_); return $file if -r($file); } return false; } sub configure($$) { my($dir, $prefix) = @_; # find a configure script my($cfg); # usual Perl elm for $cfg (qw[configure configure.gnu Configure]) { my($confprog) = path($dir, $cfg); if(-x($confprog)) { my($self) = false; my($builddir) = false; my($bogus) = false; my($twowordprefix) = false; my($noprefix) = false; my(%confopts); patch { $self ||= /this is toast 89a72ef9c34e1fd4927afa36c9be8b15/; $twowordprefix ||= /^\s*prefix=\$2; shift;;$/; # ocaml $twowordprefix ||= / --prefix dir /; # doxygen $builddir ||= /error: you must configure in a separate build/; # glibc $builddir||=m!\. \$srcdir/build-tools/scripts/configure-top !;#arch/tla $bogus ||= /There is no .+configure.+ script .+Instead/i; # libpng $bogus ||= /configure is no longer used - just type /i; # cdrtools s/(^\s*)read\s+(acceptance)(\s*$)/$1$2=yes$3/; # qt license s/^if test ! -t 0; /if false; /; # elm $noprefix = $confopts{"-d"} = 1 if /^\s*-d\) shift; fastread='y/; # elm if(reconfigure) { s/^\s*AUTOTHREAD=no\s+\#+ leave off for now/\#$&/; # qt multithreaded my($opt); # glibc; libjpeg; others? for $opt (qw[--enable-add-ons --enable-shared]) { $confopts{$opt} ||= / \Q$opt\E\b($|[^-])/; } $confopts{$1} = 1 if /"configure (-\S+)"; by default a static/;#zlib } } $confprog; $noprefix ||= -x(path($dir, "installnetpbm")); # netpbm 10.19 my(@confcmd) = ("./$cfg", "--prefix=$prefix"); if(!$self) { next if $bogus; @confcmd = ("./$cfg", "--prefix", $prefix) if $twowordprefix; @confcmd = ("./$cfg") if $noprefix; push(@confcmd, sort(grep($confopts{$_}, keys(%confopts)))); if($builddir) { $confcmd[0] = "../$cfg"; $dir = path($dir, "$myname-build.d"); md($dir); } elsif($cfg eq "configure.gnu") { # force perl to install files it thinks are already installed # (5.8.2 and later support DESTDIR and should no longer need this) optpatch { s/^use File::Compare;$/sub compare(\$\$){1}; #$genby/ } path($dir, "installperl"); } } cdrun($dir, @confcmd); return $dir; } } for(qw[build_unix build.unix]) # e.g. Sleepycat DB and nvi, respectively { my($bubuilddir) = path($dir, $_); my($buconfprog) = path($dir, "dist", "configure"); return $bubuilddir if -d($bubuilddir) && -x($buconfprog) && cdrun($bubuilddir, "../dist/configure", "--prefix=$prefix"); } # configure script might be hidden in a "source" subdirectory (e.g. samba) for(qw[source src]) { my($subdir) = path($dir, $_); my($confprog) = path($subdir, "configure"); return &configure($subdir, $prefix) if -x($confprog); } # if this XFree86, we have to set ProjectRoot in xc/config/cf/site.def... optpatch { s!(\#\s*define\s+ProjectRoot\s+)(/usr/X\S+)!$1$prefix!; } path($dir, qw[config cf site.def]); # if there's a Makefile, just use that return $dir if makefile($dir); # OK, how about an Imakefile? my($imf) = path($dir, "Imakefile"); return $dir if -r($imf) && cdrun($dir, "xmkmf"); # perl modules have a Makefile.PL script that spits out a Makefile my($mfpl) = path($dir, "Makefile.PL"); return $dir if -r($mfpl) && cdrun($dir, "perl", $mfpl); # Terrible, terrible hack to allow glibc+linuxthreads to compile: # Look for a situation where we have multiple subdirectories, but no files, # and only one of the subdirectories contains an executable configure script # that actually does something. If that happens, move the other directories # into the directory with the non-broken configure script and run that. # Otherwise one of the rules below will grab a Makefile from a random # subdirectory and try to use it, which doesn't work. my($newdir); my(@addons); my($addonsok) = whiledir { push(@addons, $_); my($abs) = path($dir, $_); return false if !-d($abs) || -l($abs); my($cfg) = path($abs, "configure"); if(-x($cfg)) { return true if whilefile { /^\s*(\#|exit|$)/ } $cfg; return false if $newdir; $newdir = $abs; pop(@addons); } true; } $dir; if($addonsok && @addons && $newdir) { mv(path($dir, $_), path($newdir, $_)) foreach @addons; return &configure($newdir, $prefix); } # Write a custom Makefile for Sun J2SDK (grumble grumble). if(-x(path($dir, qw[jre bin java]))) { my($mftext) = "# $genby" . q{ all: for i in "" `find . -name '*.pack'`; do [ -n "$$i" ] && \ lib/unpack $$i "`dirname $$i`/`basename $$i .pack`.jar"; rm -f $$i; done install: tar c */ | (cd $(DESTDIR)/usr && tar x) }; $mftext =~ s/^ /\t/gm; writefile(path($dir, "Makefile"), $mftext); return $dir; } # look for Unixish or OS-specific Makefile in a subdir and mv it here # e.g. zip -> unix/Makefile; libpng -> scripts/makefile.linux # note that this rule would incorrectly grab a spurious subdirectory # Makefile from glibc+linuxthreads or j2sdk 1.5 if the above rules didn't # come first return $dir unless dfs ( $dir, sub { true }, sub { my($rel) = @_; return true unless $rel =~ /makefile/i; return true unless $rel =~ /$^O|\bunix/i; mv($_, path($dir, "Makefile")); return false; }, sub { true } ); # Maybe there's just an install script of some kind that we need to run. # Create an appropriate Makefile. for(qw[install.sh INSTALL install.pl]) { my($script) = path($dir, $_); if(-x($script)) { my(@cmd) = "./$_"; my($isscript, $useprefix, $prefixenvvar); patch { $isscript ||= /^#!/; $useprefix ||= /--prefix=/; s/\bOutputLicense\b/true/g unless /\(/; # acroread $prefixenvvar = $1 if /\b(InstallDir)\b/; # acroread } $script; next unless $isscript; unshift(@cmd, "$prefixenvvar=$prefix") if defined($prefixenvvar); push(@cmd, "--prefix=$prefix") if $useprefix; writefile(path($dir, "Makefile"), "# $genby\n\nall:\n\ttrue\n\ninstall:\n\t@cmd\n"); return $dir; } } # try looking in promisingly-named subdirectories -d && return &configure($_, $prefix) foreach map(path($dir, $_), qw[src]); # construct a Makefile for various intransigent binary packages my(%file2dir); %file2dir = () unless whiledir { my($name) = @_; local($_) = path($dir, $name); return false if !-f; return $file2dir{$name} = "/usr/lib/netscape/plugins" if $name =~ /flashplayer\.(xpt|so)$/; # flash player 6 return $file2dir{$name} = "/bin" if $name =~ /^[a-z]+$/ && -x && -B; # upx return $file2dir{$name} = "/man/man1" if $name =~ /^[a-z]+\.1$/ && !-x && -T; $name =~ /readme|flashplayer-installer|^[A-Z]+$|\.(doc|html)$/; } $dir; if(%file2dir) { my($mftext) = "# $genby\nPREFIX=$prefix\nall:\n\ttrue\n\ninstall:\n"; for(sort(keys(%file2dir))) { my($d) = $file2dir{$_}; $mftext .= "\tmkdir -p $d\n\tcp $_ \$(DESTDIR)\$(PREFIX)$d\n"; } writefile(path($dir, "Makefile"), $mftext); return $dir; } # getting desperate now -- try to construct a Makefile from thin air my(@prog, @sources, @man); dfs ( $dir, sub { true }, sub { my($file) = @_; if(/\.c$/) { push(@sources, $file); push(@prog, stripext(basename($_))) if grep { /\bmain\b/ } readfile($_); } push(@man, $file) if /\.1$/ || ($file =~ s/\.man$/.1/); true; }, sub { true } ); # give up if our still-hypothetical Makefile is looking too implausible error("can't figure out how to compile") unless scalar(@prog) == 1 && @sources && scalar(@sources) < 100; my($mftext) = qq[# $genby PREFIX=$prefix BINDIR=\$(PREFIX)/bin MANDIR=\$(PREFIX)/man/man1 @prog: @sources install: @prog \tcp @prog \$(BINDIR) ]; $mftext .= "\tcp @man \$(MANDIR)\n" if @man; writefile(path($dir, "Makefile"), $mftext); return $dir; } sub make($) { my($dir) = @_; my($mf) = makefile($dir) || error("no Makefile found"); my($abort) = false; my($ok) = true; my($systype) = false; my($lk) = false; # blatant special case for the Linux kernel my($xf86) = false; # blatant special case for XFree86 my($please) = ""; # blatant special case for xdaliclock my($figlet, $ocaml); # further eponymous special case my($menuconfig); # busybox, uClibc explain("examining $mf..."); patch { $systype ||= /You must specify the system which you want to compil/;#mpg123 $systype ||= /If you're not sure about the characteristics of your /;#unzip $systype ||= /^\t\@echo 'Choose target:'$/; # plugger 5.0 $lk ||= /^do-it-all:.*\bvmlinux\b/; $xf86 ||= /Please use make World/i; $ocaml ||= /^\# For users who don't read the INSTALL file$/; #thanks ocaml! $please = $1 if / please make one of: "([^"]+)"/; $abort ||= /^all:[^\#]*\binstall\b/; # all implies install (e.g. man-pages) s/^DEFAULTFONTDIR = fonts$/# $&/ if $figlet; $figlet ||= m!^DEFAULTFONTDIR = /usr.*/figlet$!; $menuconfig ||= m!^all: menuconfig$!; } $mf; return false if $abort; my(@prog) = "make"; my(@targets); @targets = $^O if $systype; @targets = qw[oldconfig dep bzImage] if $lk; @targets = qw[oldconfig all] if $menuconfig; @targets = "World" if $xf86; @targets = qw[world opt] if $ocaml; @targets = $please if $please; return cdrun($dir, @prog) unless @targets; cdrun($dir, @prog, $_) foreach @targets; return true; } sub install($$) { my($makedir, $rootdir) = @_; my($mf) = makefile($makedir) || error("no Makefile found"); my(@trace, $man, $subdir, $netpbm, $usedestdir, $cdrtools); @trace = qw[strace -s 256 -f -F -o make.install.strace] if debugrewrite; @trace = qw[ktrace -i -f make.install.ktrace] if @trace && $^O =~ /bsd/i; local($ENV{ROOT}) = $rootdir; # lilo local($ENV{DESTDIR}) = $rootdir; # not always on command line due to libtiff local($ENV{install_root}) = $rootdir; local($ENV{PREFIX}) = $rootdir; # airhook v2 announce("export", "$_=$ENV{$_}") for (qw[ROOT DESTDIR install_root PREFIX]); my(@targets) = "install"; whilefile { $man ||= /^install\.man:/; $subdir = $1 if / to install the X version: "cd (\w+);/; # xdaliclock $netpbm ||= /^\t\@echo " make package pkgdir=DIR"$/; $usedestdir ||= /\b(DESTDIR|Makefile\.in)\b/; # atop, glibc; not libtiff! $cdrtools ||= / Due to a bug in SunPRO make we need special rules /; true } $mf; $netpbm &&= -x(path($makedir, "installnetpbm")) && path($makedir, "pkg"); return &install(path($makedir, $subdir), $rootdir) if $subdir; push(@targets, "install.man") if $man && reconfigure; # e.g. XFree86 @targets = ("package", "pkgdir=$netpbm") if $netpbm; @targets = "upgrade" if -r(path($makedir, "postfix-install")); # postfix push(@targets, "DESTDIR=$rootdir") if $usedestdir; push(@targets, "install_root=$rootdir") if $usedestdir; # glibc push(@targets, "PREFIX=$rootdir") if $usedestdir; # busybox push(@targets, "INS_BASE=$rootdir") if $cdrtools; cdrun($makedir, @trace, "make", @targets); run(@trace, "sh", "-c", "cp -R '$netpbm'/*/ '$rootdir'") if $netpbm; error("trace complete; aborting") if debugrewrite; } sub stddirs() { my(@result) = (qw[bin boot etc include info lib libexec man sbin share src var], map(path("man", "man$_"), 1..9), path(qw[var spool]), path(qw[var run]), # hotplug/2004_01_05 path(qw[share aclocal])); # librep/0.16.1 sort(@result); } sub stdlinks() { ( "usr" => ".", "local" => ".", "X11R6" => ".", "games" => "bin", "share/man" => "../man", "share/info" => "../info", ); } sub rootlinks() { my(@result) = stdlinks; push(@result, $_, "/$_") foreach (qw[dev proc tmp]); @result; } sub mkrootdir($$) { my($rootdir, $armdir) = @_; md($rootdir); md(path($rootdir, $_)) foreach stddirs; my(%link) = rootlinks; ln($link{$_}, path($rootdir, $_)) foreach keys(%link); optln(".", path($rootdir, $_)) foreach (unpath($armdir), unpath($rootdir)); } sub compilebin($$) { my($srcdir, $rootdir) = @_; my($one); my($ok); abswhiledir ( sub { $one = $one ? 0 : $_ unless defined($one); $ok ||= m!/(usr|bin|etc|lib|sbin)$!; -d }, $srcdir ) && ($ok ? mv($srcdir, $rootdir) : ($one && &compilebin($one, $rootdir))); } sub compilehelp($$$) { my($srcdir, $rootdir, $helperdir) = @_; my($makedir) = makedir($srcdir); my(%initenv) = inithelpers($srcdir, $rootdir, $helperdir); return false unless %initenv; defined($ENV{$_}) && $ENV{$_} eq $initenv{$_} || announce("export", "$_=$initenv{$_}") foreach keys(%initenv); local(%ENV) = %initenv; $makedir = configure($makedir, armdir); make($makedir); mkrootdir($rootdir, armdir); armhelpers($helperdir); install($makedir, $rootdir); !defined($ENV{$_}) ? announce("unset", $_) : ($ENV{$_} eq $initenv{$_} || announce("export", "$_=$ENV{$_}")) foreach keys(%initenv); return true; } sub fixbrokenrootlink($) { # stdlinks fool XFree86 and busybox into creating broken symlinks; fix them! my($link) = @_; my($dest) = readlink($link); defined($dest) || error("not a link: $link"); explain("found broken symlink: $link -> $dest"); while($dest =~ s!\.\./!! && !-e($link)) { rm($link); ln($dest, $link); } rm($link) unless -e($link); true; } sub fixrootfile($) { my($file) = @_; return -e($file) ? true : fixbrokenrootlink($file) if -l($file); safechmod(-x($file) ? 0555 : 0444, $file); } sub polishrootdir($$) { my($rootdir, $armdir) = @_; my(%link) = rootlinks; -l && rm($_) foreach map { path($rootdir, $_) } (keys(%link), unpath($rootdir), unpath($armdir)); optrmall(path($rootdir, "info", "dir")); my($total, %count) = 0; dfs ( $rootdir, sub { $count{$_} = $total; true }, sub { fixrootfile($_); ++$total }, sub { $count{$_} < $total ? safechmod(0555, $_) : rd($_) } ); $total || error("no files found in $rootdir"); # XFree86 4.3.0 libGL.so needs this due to some kind of bug/interaction, # as do some binary packages: my($libdir) = path($rootdir, "lib"); if(fixliblinks && -d($libdir)) { my(%liblinks); fordir { return unless /^(lib.*\.so)((\.[\d]+)+)$/; my($base, $ext) = ($1, $2); return if exists($liblinks{$base}) && length($liblinks{$base}) > length; $liblinks{$base} = $_; for $ext (split(/\./, $ext)) { next unless $ext; $base .= ".$ext"; next if $_ eq $base; $liblinks{$base} = $_; } } $libdir; my($mode) = getmode($libdir); safechmod(0777, $libdir) if %liblinks; optln($liblinks{$_}, path($libdir, $_)) for sort(keys(%liblinks)); safechmod($mode, $libdir); } } sub compile($$$) { my($srcdir, $rootdir, $helperdir) = @_; compilebin($srcdir, $rootdir) || compilehelp($srcdir, $rootdir, $helperdir); polishrootdir($rootdir, armdir); } sub clean(@) { whilebuild { my($name, $version, $build) = @_; my($builddir) = pkgpath($name, $version, $build); return rmall($builddir) if isbroken($name, $version, $build); return true unless isbuilt($name, $version, $build); my($srcdir) = path($builddir, srcdir); my($helperdir) = path($builddir, helperdir); optrmall($srcdir, $helperdir); } @_; return true; } sub purge(@) { my($name, $version, $build, @urls) = @_; $build && error; for $name (allnames($name)) { for $version (allversions($name, $version)) { my($archivedir) = path(pkgpath($name, $version), archivedir); optrmall($archivedir); } } return true; } sub rebuild(@) { my($name, $version, $build, @urls) = @_; $build && error; ($name, $version) = get(@_) if !isstored($name, $version); $name || error; $version || error; clean($name, $version) if autoclean; my($verdir) = pkgpath($name, $version); my(@allbuilds) = allbuilds($name, $version); $build = @allbuilds ? max(@allbuilds) : 0; my($errmsg, $builddir) = true; while($errmsg) { $build++; $builddir = path($verdir, $build); $errmsg = mkdir($builddir, 0777) ? false : $!; error("mkdir $builddir: $errmsg") if $errmsg && !-d($builddir); } announce("mkdir", $builddir); if(superuser) { my($uid, $gid) = getuidgid; announce("chown", username, $builddir); chown($uid, $gid, $builddir) || error("chown $uid:$gid $builddir: $!"); } local(*CHILD); my($pid); if(!($pid = open(CHILD, "-|"))) # child { defined($pid) || error("fork: $!"); my(@times) = gettimes; open(STDERR, ">&STDOUT") || error("dup stdout: $!"); setopt("quiet", false); nice(10); dropprivs; showprebuildinfo($name, $version, $build); yes; my($archivedir) = path($verdir, archivedir); my($srcdir) = path($builddir, srcdir); my($helperdir) = path($builddir, helperdir); my($rootdir) = path($builddir, rootdir); my($armdirlink) = path($builddir, armdirlink); ln(armdir, $armdirlink); md($srcdir); extract($archivedir, $srcdir); compile($srcdir, $rootdir, $helperdir); showtimedeltas(@times); exit(0); } $SIG{INT} = "IGNORE"; my($tmplogname) = addtmp(path($builddir, buildlog)); local(*LOG); safeopen(*LOG, ">", $tmplogname); while() { print LOG $_; chomp; print(" $_\n") unless quiet; } my($success) = close(CHILD); $SIG{INT} = "DEFAULT"; my($msg) = "child returned $?"; close(LOG) || error("close $tmplogname for write: $!"); if($success && superuser) { announce("chown", "-Rh", "root:root", $builddir); dfs ( $builddir, sub { chown(0, 0, $_) || ($success = !($msg = "chown $_: $!")) }, sub { if(!-l) { return chown(0, 0, $_) || ($success = !($msg = "chown $_: $!")) } else { my($source) = readlink; error("readlink $_: $!") unless defined($source); return (unlink || ($success = !($msg = "unlink $_: $!"))) && (symlink($source, $_) || ($success = !($msg = "symlink $source $_: $!"))); } }, sub { true } ); } my($logname) = path($builddir, $success ? buildlog : brokenlog); mv($tmplogname, $logname); $success || error($msg); clean($name, $version, $build) if autoclean; purge($name, $version) if autopurge; arm($name, $version, $build) if autoarm && isarmedmatch($name, $version); if(autodemolish || autoremove) { my($aversion, $abuild); for $aversion (allversions($name, crossversion ? undef : $version)) { if(autodemolish) { for $abuild (allbuilds($name, $aversion)) { next if $aversion eq $version && $abuild == $build; next if skipmismatched && ismismatched($name, $aversion, $abuild); next if !autodisarm && isarmed($name, $aversion, $abuild); demolish($name, $aversion, $abuild); } } if(autoremove) { next if $aversion eq $version; next if !autodisarm && isarmed($name, $aversion); remove($name, $aversion); } } } ($name, $version, $build); } sub build(@) { my($name, $version, $build, @urls) = @_; $build && error; $build = defined($version) ? latestbuilt($name, $version) : undef; return $build ? ($name, $version, $build) : rebuild(@_); } ############################################################################## sub displace($) { local($_) = @_; if(-e || -l) { error("refusing to displace directory: $_") if -d && !-l; my($offname) = addoff($_); &displace($offname); mv($_, $offname); } $_; } sub replace($) { my($on) = @_; rm($on); my($off); while(-e($off = addoff($on))) { mv($off, $on); $on = $off; } true; } sub isempty($) { my($dir) = @_; my($result) = true; whiledir { $result = false } $dir; $result; } BEGIN { my(%locks); sub lock($) { my($path) = @_; error("$path already locked") if $locks{$path}; local(*LOCK); safeopen(*LOCK, "<", $path); $locks{$path} = *LOCK{IO}; return unless useflock; if(!flock(LOCK, 6)) # LOCK_EX | LOCK_NB { explain("waiting for lock on $path"); flock(LOCK, 2) || error("flock $path: $!"); # LOCK_EX } } sub unlock($) { my($path) = @_; my($lock) = $locks{$path}; error("$path not locked") unless $lock; flock($lock, 8) || error("unlock $path: $!") if useflock; # LOCK_UN close($lock) || error("close $path: $!"); delete($locks{$path}); } } sub rebuildinfodir($) { my($armdir) = @_; my($dir) = path($armdir, "info"); return true unless -d($dir); my($dirfile) = path($dir, "dir"); # remove old links left by previous version of toast: my($oldfile) = addoff($dirfile); while(-e($oldfile) || -l($oldfile)) { rm($oldfile); $oldfile = addoff($oldfile); } my($mode) = getmode($dir); safechmod(0777, $dir); if(!infodir) { optrm($dirfile); } else { my($tmpfile) = addtmp($dirfile); optrm($tmpfile); my($tmpsuffix) = tmpsuffix; my($offsuffix) = offsuffix; abswhiledir { m!(/dir|\Q$tmpsuffix\E|\Q$offsuffix\E|-\d+(\.info)?)$! or optrun("install-info", $_, $tmpfile); } $dir; mv($tmpfile, $dirfile) if -e($tmpfile); } safechmod($mode, $dir); } sub postarm(;$$) { my($armdir, $postarmprog) = @_; $armdir = armdir unless defined($armdir); $postarmprog = postarmprog unless defined($postarmprog); rebuildinfodir($armdir); run($postarmprog) if $postarmprog; return true; } sub arm(@) { my($name, $version, $build, @urls) = @_; ($name, $version, $build) = build(@_) unless $build; return ($name, $version, $build) if isarmed($name, $version, $build); optmd(armdir); lock(armdir); my($mode) = (protect ? 0555 : 0777) & ~umask; if(getmode(armdir) != $mode) { dfs ( armdir, sub { safechmod($mode, $_) }, sub { true }, sub { true } ); } my(%link) = stdlinks; for(keys(%link)) { my($abs) = path(armdir, $_); if(!-e($abs) && !-l($abs)) { my($dir) = dirname($abs); if(!-d($dir)) { my($parent) = dirname($dir); safechmod(755, $parent); md($dir); safechmod($mode, $parent); } safechmod(0777, $dir); ln($link{$_}, $abs); safechmod($mode, $dir); } } $build || error; my($rootdir) = path(pkgpath($name, $version, $build), rootdir); dfs ( $rootdir, sub { my($dir) = optpath(armdir, $_[0]); optmd($dir); safechmod(0777, $dir); }, sub { my($target) = displace(optpath(armdir, $_[0])); relative ? relln($_, $target) : ln($_, $target); }, sub { safechmod($mode, optpath(armdir, $_[0])) } ); postarm; unlock(armdir); if(autodisarm) { my($aversion, $abuild); for $aversion (allversions($name, crossversion ? undef : $version)) { for $abuild (allbuilds($name, $aversion)) { disarm($name, $aversion, $abuild) unless $aversion eq $version && $abuild == $build; } } } ($name, $version, $build); } sub disarm(@) { my(@nvb) = @_; # see nvb comment below my(@armdirs) = allarmdirs; my($i, $armdir); for $armdir (@armdirs) { lock($armdir); whilebuild { my($name, $version, $build) = @_; my($rootdir) = path(pkgpath($name, $version, $build), rootdir); my(@dirmodes); -d($rootdir) && dfs # ignore broken packages ( $rootdir, sub { my($rel) = @_; my($armsubdir) = optpath($armdir, $rel); if(-d($armsubdir) && !-l($armsubdir)) { push(@dirmodes, getmode($armsubdir)); safechmod(0777, $armsubdir); } return true; }, sub { my($rel) = @_; my($armfile) = path($armdir, $rel); # BUG: $rel is sometimes undef? while(-e($armfile) || -l($armfile)) { return replace($armfile) if optsamefile($armfile, $_); $armfile = addoff($armfile); } return true; }, sub { my($rel) = @_; my($armsubdir) = optpath($armdir, $rel); if(-d($armsubdir) && !-l($armsubdir)) { my($mode) = pop(@dirmodes); isempty($armsubdir) ? rd($armsubdir) : safechmod($mode, $armsubdir); } return true; } ); error if @dirmodes; return true; } @nvb; # can't replace @nvb with ($n, $v, $b) due to perl 5.6.1 bug (?) postarm($armdir, ++$i == scalar(@armdirs) ? postarmprog : ""); unlock($armdir); } return true; } ############################################################################## sub upgrade(@) { my($name, $version, $build, @urls) = @_; error unless defined($name); error unless defined($version); error if defined($build); @urls = pkgurls($name, $version) unless @urls; error unless @urls; # Look for new version numbers that start with a digit and that don't # contain dashes (as in "zsh-4.0.6-doc.tar.gz"), # unless the original version number lacked those properties. my($verchar) = $version =~ /-/ ? '.' : '[^\-]'; my($verpat) = $version =~ /^\d/ ? "\\d$verchar*" : "$verchar+"; my(%linkmap, %candidates, @newurls); my($hasver) = false; for(@urls) { m!^((http|ftp)://[^\?]+/)([^\?/]*)(\?.*)?$!i || error("bad URL for upgrade: $_"); my($dirname, $basename, $query) = undeftoempty($1, $3, $4); if($basename !~ /^(.*)\Q$version\E(.*)$/) { push(@newurls, $_); # URL has no version number; use as-is } else { $hasver = true; my($pre, $post) = ($1, $2); $linkmap{$dirname} = [linksfromurl($dirname)] unless exists($linkmap{$dirname}); my(@links) = @{$linkmap{$dirname}}; my(%vermap); for(@links) { if(m!/\Q$pre\E($verpat)\Q$post\E(\?.*)?$!) { $vermap{$1} = $_; $candidates{$1} = 1; } } push(@newurls, \%vermap); } } my($newver); for(reverse(sort cmpab keys(%candidates))) { my($candidate) = $_; my($ok) = true; for(@newurls) { next unless ref; my(%vermap) = %$_; if(!exists($vermap{$candidate})) { $ok = false; last; } } if($ok) { $newver = $candidate; last; } } my($pkgname) = pkgname($name, $version); error("URLs for $pkgname don't look version-specific") unless $hasver; error("can't find consistent URLs for $pkgname") unless defined($newver); error("$pkgname appears to be the latest available version") if $version eq $newver; my(@sortvers) = sort cmpab ($version, $newver); error("only found older versions of $pkgname") if $sortvers[1] eq $version; for(@newurls) { next unless ref; my(%vermap) = %$_; $_ = $vermap{$newver}; } my(@cmdargs) = ($name, $newver, undef, @newurls); if(autoarm && isarmedmatch($name, $version)) { return arm(@cmdargs); } elsif(isbuiltmatch($name, $version)) { return build(@cmdargs); } elsif(isstored($name, $version)) { return get(@cmdargs); } else { return add(@cmdargs); } } ############################################################################## sub ensuredisarmed($;$$) { my($name, $version, $build) = @_; return true unless isarmed($name, $version, $build); error(pkgname($name, $version, $build) . " is armed") unless autodisarm; disarm($name, $version, $build); } ############################################################################## sub remove(@) { my($name, $version, $build, @urls) = @_; $name || error; ensuredisarmed($name, $version, $build); rmall(pkgpath($name, $version, $build)); } sub demolish(@) { my($name, $version, $build, @urls) = @_; whilebuild { remove(@_) } @_; } ############################################################################## sub status(@) { my($name, $version, $build, @urls) = @_; my($result) = true; my(@armdirs) = allarmdirs; for $name (allnames($name)) { print("$name\n"); for $version (allversions($name, $version)) { print(" version $version", isstored($name, $version) ? ": stored\n" : "\n"); if(showurls || @urls) { my(@haveurls) = pkgurls($name, $version); my($mismatch) = !@urls || samelist(@haveurls, @urls) ? "" : " differ from those specified"; $result = false if $mismatch; if(showurls || $mismatch) { print(" urls$mismatch:\n"); print(" $_\n") foreach @haveurls; } } for $build (allbuilds($name, $version, $build)) { my($status, @notes, @armedin); my($normalarmdir) = armdir; if(isbroken($name, $version, $build)) { $status = "broken"; } elsif(!isbuilt($name, $version, $build)) { $status = "building"; } else { push(@notes, "not clean") unless isclean($name, $version, $build); for(@armdirs) { push(@armedin, $_) if isbuildarmedin($_, $name, $version, $build); } if(ismismatched($name, $version, $build)) { $status = "mismatched"; my($armdirlink) = path(pkgpath($name, $version, $build), armdirlink); my($builtfor) = readlink($armdirlink); push(@notes, "built for $builtfor") if defined($builtfor); push(@notes, "armed") if @armedin; $normalarmdir = $armdirlink; } elsif(@armedin) { $status = "armed"; } else { $status = "built"; } } my($notetext) = @notes ? " (" . join("; ", @notes) . ")" : ""; print(" build $build: $status$notetext\n"); if(@armedin && (scalar(@armedin) > 1 || !optsamefile($armedin[0], $normalarmdir))) { print(" armed in:\n"); print(" $_\n") for @armedin; } } } } return $result; } ############################################################################## sub edit(@) { my($name, $version, $build, @urls) = @_; error unless defined($name); error if defined($build); ($name, $version) = get($name, $version, $build, @urls) unless defined($version) && isstored($name, $version); error unless defined($name); error unless defined($version); my($suffix) = "-$myname"; my($oldversion) = $version =~ /^(.*)(\Q$suffix\E\d+)$/ ? $1 : $version; error unless defined($oldversion); error(pkgname($name, $oldversion) . " is no longer stored") unless isstored($name, $oldversion); my($oldverdir) = pkgpath($name, $oldversion); my($oldarchivedir) = path($oldverdir, archivedir); my($verdir) = pkgpath($name, $version); my($archivedir) = path($verdir, archivedir); my($num) = 1; my($newversion, $newverdir); while(true) { $newversion = $oldversion . $suffix . $num; $newverdir = pkgpath($name, $newversion); last if mkdir($newverdir, 0777); error("mkdir $newverdir: $!") unless -d($newverdir); $num++; } announce("mkdir", $newverdir); my($newarchivedir) = path($newverdir, archivedir); my($editdir) = path($newverdir, editdir); my($olddir) = path($editdir, "old"); my($newdir) = path($editdir, "new"); my($patchfilename) = "edit.patch"; my($patchfile) = path($editdir, $patchfilename); md($editdir, $olddir, $newdir); if(superuser) { my($uid, $gid) = getuidgid; announce("chown", username, $olddir, $newdir); chown($uid, $gid, $olddir, $newdir) || error("chown $uid:$gid $olddir $newdir: $!"); } local(*PATCH); safeopen(*PATCH, ">", $patchfile); my($pid) = fork; error("fork: $!") unless defined($pid); if(!$pid) # child { dropprivs; extract($oldarchivedir, $olddir); extract($archivedir, $newdir); my($prog) = length(editprog) ? editprog : exists($ENV{SHELL}) ? $ENV{SHELL} : "/bin/sh"; cdrun(makedir($newdir), $prog); local(*DIFF, $_); openprog(*DIFF, "diff", "-urN", $olddir, $newdir); while() { s!^(\+\+\+|---) (\Q$olddir\E|\Q$newdir\E)/!$1 !; print PATCH $_; } close(DIFF); # ignore error close(PATCH) or error("close $patchfile: $!"); exit(0); error; } my($errmsg); waitpid($pid, 0) or error("waitpid $pid: $!"); $errmsg ||= "subprocess returned $?" unless $? == 0; close(PATCH) or error("close $patchfile: $!"); $errmsg ||= "no changes found" unless -s($patchfile); if($errmsg) { rmall($newverdir); error($errmsg); } rmall($olddir, $newdir); my(@newurls) = pkgurls($name, $oldversion); push(@newurls, fileurl($patchfile)); setpkgurls($name, $newversion, @newurls); md($newarchivedir); fordir { ln(path($oldarchivedir, $_), path($newarchivedir, $_)) } $oldarchivedir; ln($patchfile, path($newarchivedir, $patchfilename)); return build($name, $newversion, undef, @newurls); } ############################################################################## sub rename(@) { my($sname, $sversion, $sbuild, $dname, $dversion, $dbuild) = @_; defined($sname) == defined($dname) || error; defined($sversion) == defined($dversion) || error; defined($sbuild) == defined($dbuild) || error; defined($sname) || error; defined($sbuild) && !defined($sversion) && error; my($sdir) = pkgpath($sname, $sversion, $sbuild); my($ddir) = pkgpath($dname, $dversion, $dbuild); -d($sdir) || error(pkgname($sname, $sversion, $sbuild) . " does not exist"); -d($ddir) && error(pkgname($dname, $dversion, $dbuild) . " already exists"); isarmed($sname, $sversion, $sbuild) && error(pkgname($sname, $sversion, $sbuild) . " is armed"); my($namedir) = pkgpath($dname); optmd($namedir) unless $ddir eq $namedir; my($versiondir) = pkgpath($dname, $dversion); optmd($versiondir) unless $ddir eq $versiondir; mv($sdir, $ddir); my($oldnamedir) = pkgpath($sname); rmdir($oldnamedir) && announce("rmdir", $oldnamedir); return !defined($dversion) ? ($dname) : !defined($dbuild) ? ($dname, $dversion) : ($dname, $dversion, $dbuild); } ############################################################################## sub change(@) { my($name, $version, $build, @urls) = @_; error unless defined($name); error unless defined($version); error if defined($build); error unless @urls; setpkgurls($name, $version, @urls); ($name, $version); } ############################################################################## BEGIN { my($checkresult); sub failcheck(@) { print(join(": ", @_) . "\n"); $checkresult = false; return true; } sub checkbuild($$$) { my($name, $version, $build) = @_; my($path) = pkgpath($name, $version, $build); return failcheck($path, "not a directory") unless -d($path); # @todo check various fordir { /^(src|root|helpers|build\.log\.tmp|build\.log|broken\.log)$/ || failcheck(path($path, $_)); } $path; } sub checkversion($$) { my($name, $version) = @_; my($path) = pkgpath($name, $version); failcheck($path, "invalid version number") unless validversion($version); return failcheck($path, "not a directory") unless -d($path); my($urlfile) = path($path, urlfile); # @todo check urlfile my($archivedir) = path($path, archivedir); # @todo check archivedir fordir { validbuild($_) ? checkbuild($name, $version, $_) : /^(archive|url)(\.tmp)?$/ || failcheck(path($path, $_)); } $path; } sub checkname($) { my($name) = @_; my($path) = pkgpath($name); failcheck($path, "invalid package name") unless validname($name); return failcheck($path, "not a directory") unless -d($path); fordir { s/^v// ? checkversion($name, $_) : failcheck(path($path, $_)); } $path; } sub checkall() { my($path) = pkgpath; return failcheck($path, "not found") unless -e($path); return failcheck($path, "not a directory") unless -d(_); # @todo check permissions fordir { checkname($_) } $path; } sub check(@) { my($name, $version, $build) = @_; $checkresult = true; defined($build) ? checkbuild($name, $version, $build) : defined($version) ? checkversion($name, $version) : defined($name) ? checkname($name) : checkall; my($msg) = "$myname check "; $msg .= $checkresult ? "passed" : "failed"; $msg .= ": " . pkgname($name, $version, $build) if defined($name); print("$msg\n"); return $checkresult; } } ############################################################################## sub printenvcmd($$;$) { my($varname, $subdirs, $default) = @_; my(@dirs) = map(path(armdir, $_), split(/:/, $subdirs)); my($exists) = exists($ENV{$varname}); my($current) = $exists ? $ENV{$varname} : $default; my($export) = $exists ? "" : " export $varname;"; if(defined($current)) { my(%have); $have{$_} = 1 for split(/:/, $current); @dirs = grep(!$have{$_}, @dirs); } push(@dirs, $current) if defined($current); print("$varname=" . shellescape(join(":", @dirs)) . ";$export\n"); } sub env(@) { @_ && error; my($defaultman) = "/usr/man:/usr/share/man:/usr/local/man:/usr/X11R6/man"; if(!exists($ENV{MANPATH})) # don't bother unless we're going to use it... { my($out) = `man -w 2>/dev/null`; chomp($out) if defined($out); $defaultman = $out if defined($out) && $out =~ m!^/! && $out !~ /\n/; } printenvcmd("PATH", "sbin:bin"); printenvcmd("MANPATH", "man", $defaultman); printenvcmd("INFOPATH", "info", ""); printenvcmd("CPATH", "include"); printenvcmd("LIBRARY_PATH", "lib"); # there's also a LIBRARY_RUN_PATH or something that affects ld somehow... return true; } ############################################################################## sub selfopen(*) { local(*SCRIPT) = @_; my($package, $file) = caller(0); safeopen(*SCRIPT, "<", $file); } sub depodify($) { local($_) = @_; s/I\<([^\>]+)\>/\*$1\*/g; s/C\<([^\>]+)\>/\`$1\'/g; s/[A-Z]\<|\>//g; s/^=(over|back).*\r?\n?//gm; s/^=\w+\s+//gm; s/\n\n\n+/\n\n/g; $_; } sub help(@) { my(@topics) = @_; @topics = ("commands") unless @topics; print << "EOF"; $myname version $myversion -- $myurl $mycopyright $myname comes with ABSOLUTELY NO WARRANTY; for details run "$myname license". EOF local(*SCRIPT, $_); selfopen(*SCRIPT); my($usage); while(