# This file is part of NIT ( http://www.nitlanguage.org ). # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Nit package manager command line interface module picnit import opts import prompt import ini import curl import picnit_shared redef class Text # Does `self` look like a package name? # # ~~~ # assert "gamnit".is_package_name # assert "n1t".is_package_name # assert not ".".is_package_name # assert not "./gamnit".is_package_name # assert not "https://github.com/nitlang/nit.git".is_package_name # assert not "git://github.com/nitlang/nit".is_package_name # assert not "git@gitlab.com:xymus/gamnit.git".is_package_name # assert not "4it".is_package_name # ~~~ private fun is_package_name: Bool do if is_empty then return false if not chars.first.is_alpha then return false for c in chars do if not (c.is_alphanumeric or c == '_') then return false end return true end # Get package name from the Git address `self` # # Return `null` on failure. # # ~~~ # assert "https://github.com/nitlang/nit.git".git_name == "nit" # assert "git://github.com/nitlang/nit".git_name == "nit" # assert "gamnit".git_name == "gamnit" # assert "///".git_name == null # assert "file:///".git_name == "file:" # ~~~ private fun git_name: nullable String do var parts = split("/") for part in parts.reverse_iterator do if not part.is_empty then return part.strip_extension(".git") end end return null end end # Command line action, passed after `picnit` 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 "picnit install [package or git-repository]" redef fun description do return "Install a package by its name or from a git-repository" redef fun apply(args) do if args.length != 1 then print_local_help exit 1 end var package_id = args.first if package_id.is_package_name then # Ask a centralized server # TODO choose a future safe URL # TODO customizable server list # TODO parse ini file in memory var url = "https://xymus.net/picnit/{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 not found by 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 ConfigTree(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) 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) else print_error "Failed to infer the package name" exit 1 end end end private fun install_from_git(git_repo, name: String) do check_git var target_dir = picnit_lib_dir / name if target_dir.file_exists then print_error "Already installed" exit 1 end var cmd = "git clone {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 failed" exit 1 end end end # Upgrade a package class CommandUpgrade super Command redef fun name do return "upgrade" redef fun usage do return "picnit upgrade " 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 = picnit_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 "picnit uninstall " redef fun description do return "Uninstall 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 = picnit_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 # Ask confirmation 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 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 # List all installed packages class CommandList super Command redef fun name do return "list" redef fun usage do return "picnit list" redef fun description do return "List installed packages" redef fun apply(args) do var files = picnit_lib_dir.files for file in files do var ini_path = picnit_lib_dir / file / "package.ini" if verbose then print "- Reading ini file at {ini_path}" var ini = new ConfigTree(ini_path) var tags = ini["package.tags"] if tags != null then print "{file.justify(15, 0.0)} {tags}" else print file end 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 "picnit 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 this help message", "--help", "-h") # Verbose mode option var opt_verbose = new OptionBool("Print more information", "--verbose", "-v") 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 picnit_lib_dir do if "NIT_TESTING".environ == "true" then return "/tmp/picnit-test-" + "NIT_TESTING_ID".environ else return super end # Print the general help message private fun print_help do print "usage: picnit [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