#!/usr/bin/env zuzu
from std/getopt import Getopt;
from std/io import Path, STDERR, STDOUT;
from std/proc import Env, Proc;
from std/string import join, starts_with;
from std/tui import readline;
from std/zuzuzoo import Zuzuzoo;
function _usage () {
return join( "\n", [
"Usage:",
" zuzuzoo install [options] TARGET...",
" zuzuzoo remove [options] TARGET...",
" zuzuzoo remove --dist NAME...",
" zuzuzoo list [options]",
" zuzuzoo query [options] MODULE",
" zuzuzoo query --dist NAME",
" zuzuzoo verify [options] MODULE...",
" zuzuzoo verify --dist NAME...",
" zuzuzoo latest [options] MODULE",
" zuzuzoo help",
"",
"Options:",
" --dry-run show the plan without changing files",
" --force continue install when packaged tests fail",
" --no-test skip packaged tests during install",
" --global install to the global Zuzu root",
" --lib-dir DIR override the module installation directory",
" --bin-dir DIR override the script installation directory",
" --meta-dir DIR override the metadata directory",
" --cache-dir DIR cache downloaded archives in DIR",
" --lock-timeout SECONDS",
" fail if the install/remove lock is not acquired",
" --quiet suppress install/latest progress messages",
" --dist resolve targets as distribution names",
" --json print machine-readable JSON where supported",
" --yes confirm removal without prompting",
] ) _ "\n";
}
function _is_command ( value ) {
return (
value eq "install" or
value eq "remove" or
value eq "list" or
value eq "query" or
value eq "verify" or
value eq "latest" or
value eq "help"
);
}
function _has_option ( options, key ) {
return options.exists(key) and options.get(key);
}
function _tail ( items ) {
let out := [];
let i := 1;
while ( i < items.length() ) {
out.push(items[i]);
i++;
}
return out;
}
function _zuzu_command () {
let configured := Env.get("ZUZU_COMMAND");
return configured if configured != null and configured ne "";
let repo_zuzu := new Path("bin/zuzu");
return repo_zuzu.absolute().to_String()
if repo_zuzu.exists() and repo_zuzu.is_file();
return "zuzu";
}
function _operation_options ( options ) {
let out := {
zuzu_command: _zuzu_command(),
};
out.add( "dry_run", true ) if _has_option( options, "dry-run" );
out.add( "force", true ) if _has_option( options, "force" );
out.add( "no_test", true ) if _has_option( options, "no-test" );
out.add( "global", true ) if _has_option( options, "global" );
out.add( "dist", true ) if _has_option( options, "dist" );
out.add( "base_url", options.get("base-url") )
if options.exists("base-url");
out.add( "lib_dir", options.get("lib-dir") )
if options.exists("lib-dir");
out.add( "bin_dir", options.get("bin-dir") )
if options.exists("bin-dir");
out.add( "meta_dir", options.get("meta-dir") )
if options.exists("meta-dir");
out.add( "cache_dir", options.get("cache-dir") )
if options.exists("cache-dir");
out.add( "lock_timeout", options.get("lock-timeout") )
if options.exists("lock-timeout");
out.add( "progress", true )
if not _has_option( options, "quiet" );
return out;
}
function _looks_like_install_target ( target ) {
let text := "" _ target;
return true if starts_with( text, "http://" );
return true if starts_with( text, "https://" );
return true if text ~ /\.(?:tar|tar\.gz|tgz)$/;
return true if text ~ /\//;
let path := new Path(text);
return path.exists();
}
function _valid_implicit_install ( targets ) {
return false if targets.length() == 0;
for ( let target in targets ) {
return false if not _looks_like_install_target(target);
}
return true;
}
function _invalid_with_command ( command, options ) {
if ( _has_option( options, "dist" ) ) {
return "--dist is only valid with remove, query, and verify"
if command ne "remove" and command ne "query" and
command ne "verify";
}
if ( _has_option( options, "json" ) ) {
return "--json is only valid with list, query, verify, and latest"
if command ne "list" and command ne "query" and
command ne "verify" and command ne "latest";
}
if ( _has_option( options, "yes" ) ) {
return "--yes is only valid with remove"
if command ne "remove";
}
return null;
}
function _print_test_output ( result ) {
for ( let dist_result in result.get( "tests", [] ) ) {
for ( let test_result in dist_result{tests} ) {
STDOUT.print(test_result{stdout})
if test_result{stdout} ne "";
STDERR.print(test_result{stderr})
if test_result{stderr} ne "";
}
}
}
function _run_install ( zoo, targets, options, raw_options ) {
if ( targets.length() == 0 ) {
STDERR.say("install requires at least one target");
return 2;
}
options.add( "print_plan", true );
let result := zoo.install( targets, options );
_print_test_output(result);
if ( not result{ok} ) {
STDERR.say(result.get( "error", "install failed" ));
return 1;
}
if ( result.get( "forced", false ) ) {
say("Test failures ignored due to --force");
}
say( result{dry_run} ? "Dry run complete" : "Install complete" );
return 0;
}
function _confirmed_remove () {
let answer := readline(
"Remove the planned files? [y/N] ",
"n",
null,
);
return ( "" _ answer ) ~ /^(?:y|yes)$/i;
}
function _run_remove ( zoo, targets, options, raw_options ) {
if ( targets.length() == 0 ) {
STDERR.say("remove requires at least one target");
return 2;
}
let lock := zoo.acquire_lock( "remove", options );
try {
let locked_options := options;
locked_options.set( "lock", false );
let plan := zoo.plan_remove( targets, locked_options );
STDOUT.print( zoo.format_remove_plan(plan) );
if ( not plan{ok} ) {
lock.release();
return 1;
}
if ( _has_option( raw_options, "dry-run" ) ) {
lock.release();
say("Dry run complete");
return 0;
}
if ( not _has_option( raw_options, "yes" ) ) {
if ( not _confirmed_remove() ) {
lock.release();
say("Remove declined");
return 4;
}
}
let result := zoo.remove( targets, locked_options );
if ( not result{ok} ) {
lock.release();
STDERR.say("remove failed");
return 1;
}
lock.release();
say("Remove complete");
return 0;
}
catch ( Exception e ) {
lock.release();
throw e;
}
}
function _run_list ( zoo, targets, options, raw_options ) {
if ( targets.length() != 0 ) {
STDERR.say("list does not accept targets");
return 2;
}
let installed := zoo.list_installed(options);
if ( _has_option( raw_options, "json" ) ) {
say( zoo.format_json(installed) );
return 0;
}
for ( let dist in installed ) {
say(
dist{name} _ "\t" _
dist{version} _ "\t" _
dist{metadata_file}
);
}
return 0;
}
function _run_query ( zoo, targets, options, raw_options ) {
if ( targets.length() != 1 ) {
STDERR.say("query requires exactly one target");
return 2;
}
let found := _has_option( raw_options, "dist" )
? zoo.query_distribution( targets[0], options )
: zoo.query( targets[0], options );
if ( found == null ) {
STDERR.say("query target is not installed");
return 1;
}
say( zoo.format_json(found) );
return 0;
}
function _run_verify ( zoo, targets, options, raw_options ) {
if ( targets.length() == 0 ) {
STDERR.say("verify requires at least one target");
return 2;
}
let result := zoo.verify( targets, options );
if ( _has_option( raw_options, "json" ) ) {
say( zoo.format_json(result) );
}
else {
say( result{ok} ? "Verification ok" : "Verification failed" );
say( "Distributions checked: " _ result{distributions}.length() );
say( "Files checked: " _ result{checked_files}.length() );
say( "Missing files: " _ result{missing_files}.length() );
say( "Hash mismatches: " _ result{hash_mismatches}.length() );
say( "Errors: " _ result{errors}.length() );
for ( let error in result{errors} ) {
say( " - " _ error{code} _ ": " _ error{message} );
}
}
return result{ok} ? 0 : 3;
}
function _run_latest ( zoo, targets, options, raw_options ) {
if ( targets.length() != 1 ) {
STDERR.say("latest requires exactly one module target");
return 2;
}
let result := zoo.latest( targets[0], options );
if ( _has_option( raw_options, "json" ) ) {
say( zoo.format_json(result) );
return 0;
}
say( "Module: " _ result{module_name} );
say( "Latest version: " _ result{remote_version} );
say(
"Installed version: " _
( result{installed_version} == null
? "not installed"
: result{installed_version} )
);
say( "Status: " _ result{status} );
return 0;
}
function _contains_version_option ( argv ) {
for ( let arg in argv ) {
return true if arg eq "--version";
return true if starts_with( "" _ arg, "--version=" );
}
return false;
}
function _option_specs () {
return [
"help|h",
"dry-run",
"force",
"no-test",
"global",
"lib-dir=s",
"bin-dir=s",
"meta-dir=s",
"cache-dir=s",
"lock-timeout=f",
"quiet|q",
"dist",
"json",
"yes|y",
"remove=s",
"base-url=s",
];
}
function _merge_options ( base, extra ) {
for ( let key in extra.keys() ) {
base.set( key, extra.get(key) );
}
return base;
}
function _run_command ( argv ) {
let effective_argv := (
argv.length() > 0 and argv[0] eq "--"
) ? _tail(argv) : argv;
if ( _contains_version_option(effective_argv) ) {
STDERR.say(
"--version is not supported for removal; use " _
"remove --dist NAME to remove an installed distribution"
);
return 2;
}
let parsed := Getopt.parse(
effective_argv,
_option_specs(),
);
if ( not parsed{ok} ) {
STDERR.say(parsed{error});
STDERR.print(_usage());
return 2;
}
let options := parsed{options};
let rest := parsed{argv};
if ( _has_option( options, "help" ) ) {
STDOUT.print(_usage());
return 0;
}
if ( rest.length() > 0 and rest[0] eq "help" ) {
STDOUT.print(_usage());
return 0;
}
let command := "";
let targets := [];
if ( options.exists("remove") ) {
if ( rest.length() != 0 ) {
STDERR.say("--remove does not accept positional targets");
return 2;
}
command := "remove";
targets := [ options.get("remove") ];
options.add( "dist", true );
STDERR.say("--remove=NAME is deprecated; use remove --dist NAME");
}
else if ( rest.length() == 0 ) {
STDERR.print(_usage());
return 2;
}
else if ( _is_command(rest[0]) ) {
command := rest[0];
let command_parsed := Getopt.parse(
_tail(rest),
_option_specs(),
);
if ( not command_parsed{ok} ) {
STDERR.say(command_parsed{error});
STDERR.print(_usage());
return 2;
}
_merge_options( options, command_parsed{options} );
targets := command_parsed{argv};
}
else if ( _valid_implicit_install(rest) ) {
command := "install";
targets := rest;
}
else {
STDERR.say("Unknown command: " _ rest[0]);
STDERR.print(_usage());
return 2;
}
let invalid := _invalid_with_command( command, options );
if ( invalid != null ) {
STDERR.say(invalid);
return 2;
}
let operation_options := _operation_options(options);
let zoo := new Zuzuzoo(
lib_dir: operation_options.get( "lib_dir", null ),
bin_dir: operation_options.get( "bin_dir", null ),
meta_dir: operation_options.get( "meta_dir", null ),
global: operation_options.get( "global", false ),
zuzu_command: operation_options.get( "zuzu_command", "zuzu" ),
);
if ( command eq "install" ) {
return _run_install( zoo, targets, operation_options, options );
}
if ( command eq "remove" ) {
return _run_remove( zoo, targets, operation_options, options );
}
if ( command eq "list" ) {
return _run_list( zoo, targets, operation_options, options );
}
if ( command eq "query" ) {
return _run_query( zoo, targets, operation_options, options );
}
if ( command eq "verify" ) {
return _run_verify( zoo, targets, operation_options, options );
}
if ( command eq "latest" ) {
return _run_latest( zoo, targets, operation_options, options );
}
STDERR.say("Unknown command: " _ command);
return 2;
}
function __main__ ( argv ) {
try {
Proc.exit(_run_command(argv));
}
catch ( Exception e ) {
STDERR.say( "zuzuzoo: " _ e{message} );
Proc.exit(1);
};
}