curl :: native_curl
Binding of C libCurl which allow us to interact with network.core :: union_find
union–find algorithm using an efficient disjoint-set data structure
# Nit package manager command line interface
module nitpm
import opts
import prompt
import ini
import curl
import nitpm_shared
# Command line action, passed after `nitpm`
abstract class Command
# Short name of the command, specified in the command line
fun name: String is abstract
# Short usage description
fun usage: String is abstract
# Command description
fun description: String is abstract
# Apply this command consiering the `args` that follow
fun apply(args: Array[String]) do end
private var all_commands: Map[String, Command]
init do all_commands[name] = self
# Print the help message for this command
fun print_local_help
do
print "usage: {usage}"
print ""
print " {description}"
end
end
# Install a new package
class CommandInstall
super Command
redef fun name do return "install"
redef fun usage do return "nitpm install [package0[=version] [package1 ...]]"
redef fun description do return "Install packages by name, Git repository address or from the local package.ini"
# Packages installed in this run (identified by the full path)
private var installed = new Array[String]
redef fun apply(args)
do
if args.not_empty then
# Install each package
for arg in args do
# Parse each arg as an import string, with versions and commas
install_packages arg
end
else
# Install packages from local package.ini
var ini_path = "package.ini"
if not ini_path.file_exists then
print_error "Local `package.ini` not found."
print_local_help
exit 1
end
var ini = new IniFile.from_file(ini_path)
var import_line = ini["package.import"]
if import_line == null then
print_error "The local `package.ini` declares no external dependencies."
exit 0
abort
end
install_packages import_line
end
end
# Install packages defined by the `import_line`
private fun install_packages(import_line: String)
do
var imports = import_line.parse_import
for name, ext_package in imports do
install_package(ext_package.id, ext_package.version)
end
end
# Install the `package_id` at `version`
private fun install_package(package_id: String, version: nullable String)
do
if package_id.is_package_name then
# Ask a centralized server
# TODO customizable server list
# TODO parse ini file in memory
var url = "https://nitlanguage.org/catalog/p/{package_id}.ini"
var ini_path = "/tmp/{package_id}.ini"
if verbose then print "Looking for a package description at '{url}'"
var request = new CurlHTTPRequest(url)
request.verbose = verbose
var response = request.download_to_file(ini_path)
if response isa CurlResponseFailed then
print_error "Failed to contact the remote server at '{url}': {response.error_msg} ({response.error_code})"
exit 1
end
assert response isa CurlFileResponseSuccess
if response.status_code == 404 then
print_error "Package '{package_id}' not found on the server"
exit 1
else if response.status_code != 200 then
print_error "Server side error: {response.status_code}"
exit 1
end
if verbose then
print "Found a package description:"
print ini_path.to_path.read_all
end
var ini = new IniFile.from_file(ini_path)
var git_repo = ini["upstream.git"]
if git_repo == null then
print_error "Package description invalid, or it does not declare a Git repository"
exit 1
abort
end
install_from_git(git_repo, package_id, version)
else
var name = package_id.git_name
if name != null and name != "." and not name.is_empty then
name = name.to_lower
install_from_git(package_id, name, version)
else
print_error "Failed to infer the package name"
exit 1
end
end
end
private fun install_from_git(git_repo, name: String, version: nullable String)
do
check_git
var target_dir = nitpm_lib_dir / name
if version != null then target_dir += "=" + version
if installed.has(target_dir) then
# Ignore packages installed in this run
return
end
installed.add target_dir
if target_dir.file_exists then
# Warn about packages previously installed,
# install dependencies anyway in case of a previous error.
print_error "Package '{name}' is already installed"
else
# Actually install it
var cmd_branch = ""
if version != null then cmd_branch = "--branch '{version}'"
var cmd = "git clone --depth 1 {cmd_branch} {git_repo.escape_to_sh} {target_dir.escape_to_sh}"
if verbose then print "+ {cmd}"
if "NIT_TESTING".environ == "true" then
# Silence git output when testing
cmd += " 2> /dev/null"
end
var proc = new Process("sh", "-c", cmd)
proc.wait
if proc.status != 0 then
print_error "Install of '{name}' failed"
exit 1
end
end
# Recursive install
var ini = new IniFile.from_file(target_dir/"package.ini")
var import_line = ini["package.import"]
if import_line != null then
install_packages import_line
end
end
end
# Upgrade a package
class CommandUpgrade
super Command
redef fun name do return "upgrade"
redef fun usage do return "nitpm upgrade <package>"
redef fun description do return "Upgrade a package"
redef fun apply(args)
do
if args.length != 1 then
print_local_help
exit 1
end
var name = args.first
var target_dir = nitpm_lib_dir / name
if not target_dir.file_exists or not target_dir.to_path.is_dir then
print_error "Package not found"
exit 1
end
check_git
var cmd = "cd {target_dir.escape_to_sh}; git pull"
if verbose then print "+ {cmd}"
var proc = new Process("sh", "-c", cmd)
proc.wait
if proc.status != 0 then
print_error "Upgrade failed"
exit 1
end
end
end
# Uninstall a package
class CommandUninstall
super Command
redef fun name do return "uninstall"
redef fun usage do return "nitpm uninstall [-f] <package0>[=version] [package1 ...]"
redef fun description do return "Uninstall packages"
redef fun apply(args)
do
var opt_force = "-f"
var force = args.has(opt_force)
if force then args.remove(opt_force)
if args.is_empty then
print_local_help
exit 1
end
for name in args do
var clean_nitpm_lib_dir = nitpm_lib_dir.simplify_path
var target_dir = clean_nitpm_lib_dir / name
# Check validity of the package to delete
target_dir = target_dir.simplify_path
var within_dir = target_dir.has_prefix(clean_nitpm_lib_dir + "/") and
target_dir.length > clean_nitpm_lib_dir.length + 1
var valid_name = name.length > 0 and name.chars.first.is_lower
if not valid_name or not within_dir then
print_error "Package name '{name}' is invalid"
continue
end
if not target_dir.file_exists or not target_dir.to_path.is_dir then
print_error "Package not found"
exit 1
end
# Ask confirmation
if not force then
var response = prompt("Delete {target_dir.escape_to_sh}? [Y/n] ")
var accept = response != null and
(response.to_lower == "y" or response.to_lower == "yes" or response == "")
if not accept then return
end
var cmd = "rm -rf {target_dir.escape_to_sh}"
if verbose then print "+ {cmd}"
var proc = new Process("sh", "-c", cmd)
proc.wait
if proc.status != 0 then
print_error "Uninstall failed"
exit 1
end
end
end
end
# List all installed packages
class CommandList
super Command
redef fun name do return "list"
redef fun usage do return "nitpm list"
redef fun description do return "List installed packages"
redef fun apply(args)
do
var files = nitpm_lib_dir.files
var name_to_desc = new Map[String, nullable String]
var max_name_len = 0
# Collect package info
for file in files do
var ini_path = nitpm_lib_dir / file / "package.ini"
if verbose then print "- Reading ini file at {ini_path}"
var ini = new IniFile.from_file(ini_path)
var tags = ini["package.tags"]
name_to_desc[file] = tags
max_name_len = max_name_len.max(file.length)
end
# Sort in alphabetical order
var sorted_names = name_to_desc.keys.to_a
alpha_comparator.sort sorted_names
# Print with clear columns
for name in sorted_names do
var col0 = name.justify(max_name_len+1, 0.0)
var col1 = name_to_desc[name] or else ""
var line = col0 + col1
print line.trim
end
end
end
# Show general help or help specific to a command
class CommandHelp
super Command
redef fun name do return "help"
redef fun usage do return "nitpm help [command]"
redef fun description do return "Show general help message or the help for a command"
redef fun apply(args)
do
# Try first to help about a valid action
if args.length == 1 then
var command = commands.get_or_null(args.first)
if command != null then
command.print_local_help
return
end
end
print_help
end
end
redef class Sys
# General command line options
var opts = new OptionContext
# Help option
var opt_help = new OptionBool("Show help message", "-h", "--help")
# Verbose mode option
var opt_verbose = new OptionBool("Print more information", "-v", "--verbose")
private fun verbose: Bool do return opt_verbose.value
# All command line actions, mapped to their short `name`
var commands = new Map[String, Command]
private var command_install = new CommandInstall(commands)
private var command_list = new CommandList(commands)
private var command_update = new CommandUpgrade(commands)
private var command_uninstall = new CommandUninstall(commands)
private var command_help = new CommandHelp(commands)
end
redef fun nitpm_lib_dir
do
if "NIT_TESTING".environ == "true" then
return "/tmp/nitpm-test-" + "NIT_TESTING_ID".environ
else return super
end
# Print the general help message
private fun print_help
do
print "usage: nitpm <command> [options]"
print ""
print "commands:"
for command in commands.values do
print " {command.name.justify(11, 0.0)} {command.description}"
end
print ""
print "options:"
opts.usage
end
# Check if `git` is available, exit if not
private fun check_git
do
var proc = new ProcessReader("git", "--version")
proc.wait
proc.close
if proc.status != 0 then
print_error "Please install `git`"
exit 1
end
end
# Parse main options
opts.add_option(opt_help, opt_verbose)
opts.parse
var rest = opts.rest
if opt_help.value then
print_help
exit 0
end
if opts.errors.not_empty then
for error in opts.errors do print error
print ""
print_help
exit 1
end
if rest.is_empty then
print_help
exit 1
end
# Find and apply action
var action_name = rest.shift
var action = commands.get_or_null(action_name)
if action != null then
action.apply rest
else
print_help
exit 1
end
src/nitpm.nit:15,1--455,3