Files
Scoop/lib/install.ps1
T
2013-08-15 13:05:00 +10:00

376 lines
9.8 KiB
PowerShell

function cache_path($app, $version, $url) {
"$cachedir\$app#$version#$($url -replace '[^\w\.\-]+', '_')"
}
function dl_with_cache($app, $version, $url, $to) {
$cached = fullpath (cache_path $app $version $url)
if(!(test-path $cached)) {
$null = ensure $cachedir
write-host "downloading $url..." -nonewline
dl_progress $url "$cached.download"
mv "$cached.download" $cached
write-host "done"
} else { write-host "loading $url from cache..."}
cp $cached $to
}
function dl_progress($url, $to) {
$left = [console]::cursorleft
$wc = new-object net.webclient
register-objectevent $wc downloadprogresschanged progress | out-null
register-objectevent $wc downloadfilecompleted complete | out-null
try {
$wc.downloadfileasync($url, $to)
function is_complete {
try { get-event complete -ea stop; $true } catch { $false }
}
$last_p = -1
while(!(is_complete)) {
$e = wait-event progress
remove-event progress
$p = $e.sourceeventargs.progresspercentage
if($p -ne $last_p) {
[console]::cursorleft = $left
write-host "$p%" -nonewline
$last_p = $p
}
}
remove-event complete
} finally {
remove-event *
unregister-event progress
unregister-event complete
$wc.cancelasync()
$wc.dispose()
}
[console]::cursorleft = $left
}
function dl_urls($app, $version, $manifest, $architecture, $dir) {
# can be multiple urls: if there are, then msi or installer should go last,
# so that $fname is set properly
$urls = @(url $manifest $architecture)
$fname = $null
foreach($url in $urls) {
$fname = split-path $url -leaf
dl_with_cache $app $version $url "$dir\$fname"
check_hash "$dir\$fname" $url $manifest $architecture
# extract
if($fname -match '\.zip$') { # unzip
write-host "extracting..." -nonewline
# use tmp directory and copy so we can prevent 'folder merge' errors when multiple URLs
$null = mkdir "$dir\_scoop_unzip"
unzip "$dir\$fname" "$dir\_scoop_unzip" (extract_dir $manifest $architecture)
cp "$dir\_scoop_unzip\*" "$dir" -r -force
rm -r -force "$dir\_scoop_unzip"
rm "$dir\$fname"
write-host "done"
} elseif(requires_7zip $fname) { # 7zip
if(!(7zip_installed)) {
warn "aborting: you'll need to run 'scoop uninstall $app' to clean up"
abort "7-zip is required. you can install it with 'scoop install 7zip'"
}
$to = $dir
$extract_dir = extract_dir $manifest $architecture
if($extract_dir) {
$to = "$dir\_scoop_extract"
}
extract_7zip "$dir\$fname" $to
if($extract_dir) {
gci "$to\$extract_dir" -r | mv -dest "$dir" -force
rm -r -force "$to"
}
}
}
$fname # returns the last downloaded file
}
function is_in_dir($dir, $file) {
$file = "$(fullpath $file)"
$dir = "$(fullpath $dir)"
$file -match "^$([regex]::escape("$dir\"))"
}
# hashes
function hash_for_url($manifest, $url, $arch) {
$hashes = @(hash $manifest $arch) | ? { $_ -ne $null };
if($hashes.length -eq 0) { return $null }
$urls = @(url $manifest $arch)
$index = [array]::indexof($urls, $url)
if($index -eq -1) { abort "couldn't find hash in manifest for $url" }
@($hashes)[$index]
}
function check_hash($file, $url, $manifest, $arch) {
$hash = hash_for_url $manifest $url $arch
if(!$hash) {
warn "warning: no hash in manifest. sha256 is:`n$(compute_hash (fullpath $file) 'sha256')"
return
}
write-host "checking hash..." -nonewline
$type, $expected = $hash.split(':')
if(!$expected) {
# no type specified, assume sha256
$type, $expected = 'sha256', $type
}
if(@('md5','sha1','sha256') -notcontains $type) { "hash type $type isn't supported" }
$actual = compute_hash (fullpath $file) $type
if($actual -ne $expected) {
abort "hash check failed for $url. expected: $($expected), actual: $($actual)!"
}
write-host "ok"
}
function compute_hash($file, $algname) {
$alg = [system.security.cryptography.hashalgorithm]::create($algname)
$fs = [system.io.file]::openread($file)
try {
$hexbytes = $alg.computehash($fs) | % { $_.tostring('x2') }
[string]::join('', $hexbytes)
} finally {
$fs.dispose()
$alg.dispose()
}
}
# for dealing with installers
function args($config, $dir) {
if($config) { return $config | % { (format $_ @{'dir'=$dir}) } }
@()
}
function run($exe, $arg, $msg, $continue_exit_codes) {
write-host $msg -nonewline
try {
$proc = start-process $exe -wait -ea stop -passthru -arg $arg
if($proc.exitcode -ne 0) {
if($continue_exit_codes -and ($continue_exit_codes.containskey($proc.exitcode))) {
warn $continue_exit_codes[$proc.exitcode]
return $true
}
write-host "exit code was $($proc.exitcode)"; return $false
}
} catch {
write-host -f darkred $_.exception.tostring()
return $false
}
write-host "done"
return $true
}
function run_installer($fname, $manifest, $architecture, $dir) {
# MSI or other installer
$msi = msi $manifest $architecture
$installer = installer $manifest $architecture
if($msi) {
install_msi $fname $dir $msi
} elseif($installer) {
install_exe $fname $dir $installer
}
}
function install_msi($fname, $dir, $msi) {
$msifile = "$dir\$(coalesce $msi.file "$fname")"
if(!(is_in_dir $dir $msifile)) {
abort "error in manifest: MSI file $msifile is outside the app directory"
}
if(!($msi.code)) { abort "error in manifest: couldn't find MSI code"}
$logfile = "$dir\install.log"
$arg = @("/i `"$msifile`"", '/norestart', "/lvp `"$logfile`"", "TARGETDIR=`"$dir`"",
"INSTALLDIR=`"$dir`"") + @(args $msi.args $dir)
if($msi.silent) { $arg += '/qn', 'ALLUSERS=2', 'MSIINSTALLPERUSER=1' }
else { $arg += '/qb-!' }
$continue_exit_codes = @{ 3010 = "a restart is required to complete installation" }
$installed = run 'msiexec' $arg "running installer..." $continue_exit_codes
if(!$installed) {
abort "installation aborted. you might need to run 'scoop uninstall $app' before trying again."
}
rm $logfile
rm $msifile
}
function install_exe($fname, $dir, $installer) {
$exe = "$dir\$(coalesce $installer.exe "$fname")"
if(!(is_in_dir $dir $exe)) {
abort "error in manifest: installer $exe is outside the app directory"
}
$arg = args $installer.args $dir
$installed = run $exe $arg "running installer..."
if(!$installed) {
abort "installation aborted. you might need to run 'scoop uninstall $app' before trying again."
}
rm $exe
}
function run_uninstaller($manifest, $architecture, $dir) {
$msi = msi $manifest $architecture
$uninstaller = uninstaller $manifest $architecture
if($msi -or $uninstaller) {
$exe = $null; $arg = $null; $continue_exit_codes = @{}
if($msi) {
$code = $msi.code
$exe = "msiexec";
$arg = @("/norestart", "/x $code")
if($msi.silent) {
$arg += '/qn', 'ALLUSERS=2', 'MSIINSTALLPERUSER=1'
} else {
$arg += '/qb-!'
}
$continue_exit_codes.1605 = 'not installed, skipping'
$continue_exit_codes.3010 = 'restart required'
} elseif($uninstaller) {
$exe = "$dir\$($uninstaller.exe)"
$arg = args $uninstaller.args
if(!(is_in_dir $dir $exe)) {
warn "error in manifest: installer $exe is outside the app directory, skipping"
$exe = $null;
} elseif(!(test-path $exe)) {
warn "uninstaller $exe is missing, skipping"
$exe = $null;
}
}
if($exe) {
$uninstalled = run $exe $arg "running uninstaller..." $continue_exit_codes
if(!$uninstalled) { abort "uninstallation aborted." }
}
}
}
function create_shims($manifest, $dir) {
$manifest.bin | ?{ $_ -ne $null } | % {
echo "creating shim for $_"
# check valid bin
$bin = "$dir\$_"
if(!(is_in_dir $dir $bin)) {
abort "error in manifest: bin '$_' is outside the app directory"
}
if(!(test-path $bin)) { abort "can't shim $_`: file doesn't exist"}
shim "$dir\$_"
}
}
function rm_shims($manifest) {
$manifest.bin | ?{ $_ -ne $null } | % {
$shim = "$shimdir\$(strip_ext(fname $_)).ps1"
$shim_cmd = "$(strip_ext $shim).cmd"
if(!(test-path $shim)) { # handle no shim from failed install
warn "shim for $_ is missing, skipping"
} else {
echo "removing shim for $_"
rm $shim
}
if(test-path $shim_cmd) { rm $shim_cmd }
}
}
# for installers that insist on changing path
function ensure_install_dir_not_in_path($dir) {
$user_path = (env 'path')
$machine_path = [environment]::getEnvironmentVariable('path', 'Machine')
$fixed, $removed = find_dir_or_subdir $user_path "$dir"
if($removed) {
$removed | % { "installer added $(friendly_path $_) to path, removing"}
env 'path' $fixed
}
$fixed, $removed = find_dir_or_subdir $machine_path "$dir"
if($removed) {
$removed | % { warn "installer added $_ to system path: you might want to remove this manually (requires admin permission)"}
}
}
function find_dir_or_subdir($path, $dir) {
$dir = $dir.trimend('\')
$fixed = @()
$removed = @()
$path.split(';') | % {
if($_) {
if(($_ -eq $dir) -or ($_ -like "$dir\*")) { $removed += $_ }
else { $fixed += $_ }
}
}
return [string]::join(';', $fixed), $removed
}
function add_env_path($manifest, $dir) {
$manifest.add_env_path | ? { $_ } | % {
$path_dir = "$dir\$($_)"
if(!(is_in_dir $dir $path_dir)) {
abort "error in manifest: add_to_path '$_' is outside the app directory"
}
ensure_in_path $path_dir
}
}
function rm_env_path($manifest, $dir) {
# remove from path
$manifest.add_env_path | ? { $_ } | % {
$path_dir = "$dir\$($_)"
remove_from_path $path_dir
}
}
function set_env($manifest, $dir) {
if($manifest.set_env) {
$manifest.set_env | gm -member noteproperty | % {
$name = $_.name;
$val = format $manifest.set_env.$($_.name) @{ "dir" = $dir }
env $name $val
sc env:\$name $val
}
}
}
function rm_env($manifest) {
if($manifest.set_env) {
$manifest.set_env | gm -member noteproperty | % {
$name = $_.name;
env $name $null
if(test-path env:\$name) { rm env:\$name }
}
}
}
function post_install($manifest) {
$manifest.post_install | ? {$_ } | % {
echo "running post-install script..."
iex $_
}
}
function show_notes($manifest) {
if($manifest.notes) {
echo "Notes"
echo "-----"
echo $manifest.notes
}
}