Nit package manager command line interface

Introduced classes

abstract class Command

nitc :: Command

Command line action, passed after nitpm
class CommandHelp

nitc :: CommandHelp

Show general help or help specific to a command
class CommandInstall

nitc :: CommandInstall

Install a new package
class CommandList

nitc :: CommandList

List all installed packages
class CommandUninstall

nitc :: CommandUninstall

Uninstall a package
class CommandUpgrade

nitc :: CommandUpgrade

Upgrade a package

Redefined classes

redef class Sys

nitc :: nitpm $ Sys

The main class of the program.

All class definitions

abstract class Command

nitc $ Command

Command line action, passed after nitpm
class CommandHelp

nitc $ CommandHelp

Show general help or help specific to a command
class CommandInstall

nitc $ CommandInstall

Install a new package
class CommandList

nitc $ CommandList

List all installed packages
class CommandUninstall

nitc $ CommandUninstall

Uninstall a package
class CommandUpgrade

nitc $ CommandUpgrade

Upgrade a package
redef class Sys

nitc :: nitpm $ Sys

The main class of the program.
package_diagram nitc::nitpm nitpm opts opts nitc::nitpm->opts prompt prompt nitc::nitpm->prompt ini ini nitc::nitpm->ini curl curl nitc::nitpm->curl nitc::nitpm_shared nitpm_shared nitc::nitpm->nitc::nitpm_shared core core opts->core prompt->core ini->core curl->core json json curl->json nitc::nitpm_shared->core ...core ... ...core->core ...json ... ...json->json a_star-m a_star-m a_star-m->nitc::nitpm

Ancestors

module abstract_collection

core :: abstract_collection

Abstract collection classes and services.
module abstract_text

core :: abstract_text

Abstract class for manipulation of sequences of characters
module array

core :: array

This module introduces the standard array structure.
module bitset

core :: bitset

Services to handle BitSet
module bytes

core :: bytes

Services for byte streams and arrays
module circular_array

core :: circular_array

Efficient data structure to access both end of the sequence.
module codec_base

core :: codec_base

Base for codecs to use with streams
module codecs

core :: codecs

Group module for all codec-related manipulations
module collection

core :: collection

This module define several collection classes.
module core

core :: core

Standard classes and methods used by default by Nit programs and libraries.
module environ

core :: environ

Access to the environment variables of the process
module error

core :: error

Standard error-management infrastructure.
module exec

core :: exec

Invocation and management of operating system sub-processes.
module file

core :: file

File manipulations (create, read, write, etc.)
module fixed_ints

core :: fixed_ints

Basic integers of fixed-precision
module fixed_ints_text

core :: fixed_ints_text

Text services to complement fixed_ints
module flat

core :: flat

All the array-based text representations
module gc

core :: gc

Access to the Nit internal garbage collection mechanism
module hash_collection

core :: hash_collection

Introduce HashMap and HashSet.
module iso8859_1

core :: iso8859_1

Codec for ISO8859-1 I/O
module kernel

core :: kernel

Most basic classes and methods.
module list

core :: list

This module handle double linked lists
module math

core :: math

Mathematical operations
module native

core :: native

Native structures for text and bytes
module native_curl

curl :: native_curl

Binding of C libCurl which allow us to interact with network.
module numeric

core :: numeric

Advanced services for Numeric types
module protocol

core :: protocol

module queue

core :: queue

Queuing data structures and wrappers
module range

core :: range

Module for range of discrete objects.
module re

core :: re

Regular expression support for all services based on Pattern
module ropes

core :: ropes

Tree-based representation of a String.
module sorter

core :: sorter

This module contains classes used to compare things and sorts arrays.
module stream

core :: stream

Input and output streams of characters
module text

core :: text

All the classes and methods related to the manipulation of text entities
module time

core :: time

Management of time and dates
module union_find

core :: union_find

union–find algorithm using an efficient disjoint-set data structure
module utf8

core :: utf8

Codec for UTF-8 I/O

Parents

module curl

curl :: curl

Data transfer powered by the native curl library
module ini

ini :: ini

Read and write INI configuration files
module nitpm_shared

nitc :: nitpm_shared

Services related to the Nit package manager
module opts

opts :: opts

Management of options on the command line
module prompt

prompt :: prompt

Basic services to display a prompt

Children

module a_star-m

a_star-m

# 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