mirror of
https://git.planet-casio.com/Lephenixnoir/GiteaPC.git
synced 2024-12-28 04:23:40 +01:00
most things, minus dependencies, updates, and auto-install
This commit is contained in:
commit
5406afa497
10 changed files with 900 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
@ -0,0 +1 @@
|
||||||
|
__pycache__
|
16
Makefile
Normal file
16
Makefile
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
PREFIX ?= $(HOME)/.local
|
||||||
|
VERSION = 1.0
|
||||||
|
|
||||||
|
install: $(bin)
|
||||||
|
install -d $(PREFIX)/bin
|
||||||
|
sed -e 's*%PREFIX%*$(PREFIX)*; s*%VERSION%*$(VERSION)*' giteapc.py > $(PREFIX)/bin/giteapc
|
||||||
|
chmod +x $(PREFIX)/bin/giteapc
|
||||||
|
install -d $(PREFIX)/lib/giteapc/giteapc
|
||||||
|
install giteapc/*.py $(PREFIX)/lib/giteapc/giteapc
|
||||||
|
|
||||||
|
uninstall:
|
||||||
|
rm -f $(PREFIX)/bin/giteapc
|
||||||
|
rm -rf $(PREFIX)/lib/giteapc
|
||||||
|
@ echo "note: repositories cloned by GiteaPC have not been removed"
|
||||||
|
|
||||||
|
.PHONY: install uninstall
|
149
giteapc.py
Normal file
149
giteapc.py
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
#! /usr/bin/env python3
|
||||||
|
|
||||||
|
"""
|
||||||
|
GiteaPC is an automated installer/updated for repositories of the Planète Casio
|
||||||
|
Gitea forge (https://gitea.planet-casio.com/). It is mainly used to set up
|
||||||
|
installs of the add-in development fxSDK.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# Install prefix (inserted at compile-time)
|
||||||
|
PREFIX = "%PREFIX%"
|
||||||
|
# Program version (inserted at compile-time)
|
||||||
|
VERSION = "%VERSION%"
|
||||||
|
|
||||||
|
import sys
|
||||||
|
sys.path.append(PREFIX + "/lib/giteapc")
|
||||||
|
|
||||||
|
import giteapc.repo
|
||||||
|
from giteapc.util import *
|
||||||
|
from giteapc.config import REPO_FOLDER, PREFIX_FOLDER
|
||||||
|
import getopt
|
||||||
|
import sys
|
||||||
|
import os
|
||||||
|
|
||||||
|
# TODO:
|
||||||
|
# * @ or ~ shortcut for remote repositories to avoid the -r option?
|
||||||
|
# * build, install: repository updates
|
||||||
|
# * NetworkError for cURL
|
||||||
|
# * Handle dependencies
|
||||||
|
# * Test update logic
|
||||||
|
|
||||||
|
usage_string = """
|
||||||
|
usage: {R}giteapc{_} [{R}list{_}|{R}fetch{_}|{R}show{_}|{R}build{_}|{R}install{_}|{R}uninstall{_}] [{g}{i}ARGS...{_}]
|
||||||
|
|
||||||
|
GiteaPC is a tool to automatically clone, install and update repositories from
|
||||||
|
the Planète Casio Gitea forge. In the following commands, each {g}{i}REPOSITORY{_}
|
||||||
|
is either a full name like "Lephenixnoir/sh-elf-gcc", or a short name like
|
||||||
|
"sh-elf-gcc" when there is no ambiguity.
|
||||||
|
|
||||||
|
{R}giteapc list{_} [{R}-r{_}] [{g}{i}PATTERN{_}]
|
||||||
|
Lists all repositories on this computer. With -r, lists repositories on the
|
||||||
|
forge. A wildcard pattern can be specified to filter the results.
|
||||||
|
{R}giteapc fetch{_} [{R}--https{_}|{R}--ssh{_}] [{R}-u{_}] [{R}-f{_}] [{g}{i}REPOSITORY{_}[{R}@{_}{g}{i}VERSION{_}]{g}{i}...{_}]
|
||||||
|
Clones or fetches a repository and its dependencies. If no repository is
|
||||||
|
specified, fetches all the local repositories. HTTPS or SSH can be selected
|
||||||
|
when cloning (HTTPS by default). With -u, pulls after fetching.
|
||||||
|
{R}giteapc show{_} [{R}-r{_}] [{R}-p{_}] {g}{i}REPOSITORY...{_}
|
||||||
|
Shows the branches and tags (versions) for the specified local repositories.
|
||||||
|
With -r, show information for remote repositories on the forge.
|
||||||
|
With -p, just print the path of local repositories (useful in scripts).
|
||||||
|
{R}giteapc build{_} [{R}-i{_}] [{R}-u{_}] [{R}--skip-configure{_}] {g}{i}REPOSITORY{_}[{R}@{_}{g}{i}VERSION{_}][{R}:{_}{g}{i}CONFIG{_}]{g}{i}...{_}
|
||||||
|
Configures and builds a local repository. A specific configuration can be
|
||||||
|
requested. With -i, also installs if build is successful. --skip-configure
|
||||||
|
builds without configuring (useful for rebuilds). With -u, pulls the current
|
||||||
|
branch before building (update mode).
|
||||||
|
{R}giteapc install{_} [{R}--https{_}|{R}--ssh{_}] [{R}-u{_}] {g}{i}REPOSITORY{_}[{R}@{_}{g}{i}VERSION{_}][{R}:{_}{g}{i}CONFIG{_}]{g}{i}...{_}
|
||||||
|
Shortcut to clone (or fetch), build, and install a repository.
|
||||||
|
{R}giteapc uninstall{_} [{R}-k{_}] {g}{i}REPOSITORY...{_}
|
||||||
|
Uninstalls the build products of the specified repositories and removes the
|
||||||
|
source files. With -k, keeps the source files.
|
||||||
|
|
||||||
|
{W}Important folders{_}
|
||||||
|
|
||||||
|
{R}$GITEAPC_HOME{_} or {R}$XDG_DATA_HOME/giteapc{_} or {R}$HOME/.local/share/giteapc{_}
|
||||||
|
{W}->{_} {b}{}{_}
|
||||||
|
Storage for GiteaPC repositories.
|
||||||
|
{R}$GITEAPC_PREFIX{_} or {R}$HOME/.local{_}
|
||||||
|
{W}->{_} {b}{}{_}
|
||||||
|
Default install prefix.
|
||||||
|
""".format(REPO_FOLDER, PREFIX_FOLDER, **colors()).strip()
|
||||||
|
|
||||||
|
def usage(exitcode=None):
|
||||||
|
print(usage_string, file=sys.stderr)
|
||||||
|
if exitcode is not None:
|
||||||
|
sys.exit(exitcode)
|
||||||
|
|
||||||
|
commands = {
|
||||||
|
"list": {
|
||||||
|
"function": giteapc.repo.list,
|
||||||
|
"args": "remote:-r,--remote",
|
||||||
|
},
|
||||||
|
"fetch": {
|
||||||
|
"function": giteapc.repo.fetch,
|
||||||
|
"args": "use_ssh:--ssh use_https:--https force:-f,--force "+\
|
||||||
|
"update:-u,--update",
|
||||||
|
},
|
||||||
|
"show": {
|
||||||
|
"function": giteapc.repo.show,
|
||||||
|
"args": "remote:-r,--remote path:-p,--path-only",
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"function": giteapc.repo.build,
|
||||||
|
"args": "install:-i,--install skip_configure:--skip-configure "+\
|
||||||
|
"update:-u,--update",
|
||||||
|
},
|
||||||
|
"install": {
|
||||||
|
"function": giteapc.repo.install,
|
||||||
|
"args": "use_ssh:--ssh use_https:--https update:-u,--update",
|
||||||
|
},
|
||||||
|
"uninstall": {
|
||||||
|
"function": giteapc.repo.uninstall,
|
||||||
|
"args": "keep:-k,--keep",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
def main(argv, commands):
|
||||||
|
# Help, version, and invocations without a proper command name
|
||||||
|
if "-h" in argv or "--help" in argv:
|
||||||
|
usage(0)
|
||||||
|
if "-v" in argv or "--version" in argv:
|
||||||
|
print(f"GiteaPC {VERSION}")
|
||||||
|
return 0
|
||||||
|
if argv == [] or argv[0] not in commands:
|
||||||
|
usage(1)
|
||||||
|
|
||||||
|
args = commands[argv[0]].get("args","").split()
|
||||||
|
args = [x.split(":",1) for x in args]
|
||||||
|
args = [(name,forms.split(",")) for name, forms in args]
|
||||||
|
|
||||||
|
single = ""
|
||||||
|
double = []
|
||||||
|
for name, forms in args:
|
||||||
|
for f in forms:
|
||||||
|
if f.startswith("--"):
|
||||||
|
double.append(f[2:])
|
||||||
|
elif f[0] == "-" and len(f) == 2:
|
||||||
|
single += f[1]
|
||||||
|
else:
|
||||||
|
raise Exception("invalid argument format o(x_x)o")
|
||||||
|
|
||||||
|
# Parse arguments
|
||||||
|
try:
|
||||||
|
opts, data = getopt.gnu_getopt(argv[1:], single, double)
|
||||||
|
opts = { name: True if val == "" else val for (name,val) in opts }
|
||||||
|
except getopt.GetoptError as e:
|
||||||
|
return fatal(e)
|
||||||
|
|
||||||
|
options = {}
|
||||||
|
for (o,val) in opts.items():
|
||||||
|
for (name, forms) in args:
|
||||||
|
if o in forms:
|
||||||
|
options[name] = val
|
||||||
|
|
||||||
|
try:
|
||||||
|
return commands[argv[0]]["function"](*data, **options)
|
||||||
|
except Error as e:
|
||||||
|
return fatal(e)
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main(sys.argv[1:], commands))
|
0
giteapc/__init__.py
Normal file
0
giteapc/__init__.py
Normal file
9
giteapc/config.py
Normal file
9
giteapc/config.py
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
import os
|
||||||
|
|
||||||
|
# URL to the Gitea forge supplying the resources
|
||||||
|
GITEA_URL = "https://gitea.planet-casio.com"
|
||||||
|
# Data folder to store repositores
|
||||||
|
XDG_DATA_HOME = os.getenv("XDG_DATA_HOME", os.getenv("HOME")+"/.local/share")
|
||||||
|
REPO_FOLDER = os.getenv("GITEAPC_HOME") or XDG_DATA_HOME + "/giteapc"
|
||||||
|
# Prefix folder to install files to
|
||||||
|
PREFIX_FOLDER = os.getenv("GITEAPC_PREFIX") or os.getenv("HOME") + "/.local"
|
61
giteapc/gitea.py
Normal file
61
giteapc/gitea.py
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
from giteapc.util import *
|
||||||
|
import giteapc.repos
|
||||||
|
import giteapc.config
|
||||||
|
import subprocess
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Provide a mechanism to submit HTTP GET requests to the forge. Try to use the
|
||||||
|
# requests module if it's available, otherwise default to cURL. If none is
|
||||||
|
# available, disable network features but keep going.
|
||||||
|
_http_get = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
import requests
|
||||||
|
_http_get = requests_get
|
||||||
|
except ImportError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if _http_get is None and has_curl():
|
||||||
|
_http_get = curl_get
|
||||||
|
|
||||||
|
if _http_get is None:
|
||||||
|
warn("found neither requests nor curl, network will be disabled")
|
||||||
|
|
||||||
|
# Send a GET request to the specified API endpoint
|
||||||
|
def _get(url, params=None):
|
||||||
|
if _http_get is None:
|
||||||
|
raise Error("cannot access forge (network is disabled)")
|
||||||
|
return _http_get(giteapc.config.GITEA_URL + "/api/v1" + url, params)
|
||||||
|
|
||||||
|
# Search for repositories
|
||||||
|
def repo_search(keyword, **kwargs):
|
||||||
|
r = _get("/repos/search", { "q": keyword, **kwargs })
|
||||||
|
results = json.loads(r)['data']
|
||||||
|
results = [ giteapc.repos.RemoteRepo(r) for r in results ]
|
||||||
|
return sorted(results, key=lambda r: r.fullname)
|
||||||
|
|
||||||
|
# List all remote repositories (with topic "giteapc")
|
||||||
|
def all_remote_repos():
|
||||||
|
return repo_search("giteapc", topic=True)
|
||||||
|
|
||||||
|
# Repository info
|
||||||
|
def repo_get(fullname):
|
||||||
|
try:
|
||||||
|
r = _get(f"/repos/{fullname}")
|
||||||
|
return giteapc.repos.RemoteRepo(json.loads(r))
|
||||||
|
except NetworkError as e:
|
||||||
|
if e.status == 404:
|
||||||
|
return None
|
||||||
|
else:
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def repo_branches(r):
|
||||||
|
r = _get(r.url + "/branches")
|
||||||
|
return json.loads(r)
|
||||||
|
def repo_tags(r):
|
||||||
|
r = _get(r.url + "/tags")
|
||||||
|
return json.loads(r)
|
||||||
|
def repo_topics(r):
|
||||||
|
r = _get(r.url + "/topics")
|
||||||
|
return json.loads(r)["topics"]
|
316
giteapc/repo.py
Normal file
316
giteapc/repo.py
Normal file
|
@ -0,0 +1,316 @@
|
||||||
|
import giteapc.gitea as gitea
|
||||||
|
from giteapc.repos import LocalRepo, RemoteRepo
|
||||||
|
from giteapc.util import *
|
||||||
|
from giteapc.config import REPO_FOLDER, PREFIX_FOLDER
|
||||||
|
import fnmatch
|
||||||
|
import shutil
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
|
||||||
|
#
|
||||||
|
# Determine full repo names from short versions
|
||||||
|
#
|
||||||
|
|
||||||
|
def local_match(name):
|
||||||
|
"""Find all local repositories with the specified name."""
|
||||||
|
return [ r for r in LocalRepo.all() if r.name == name ]
|
||||||
|
|
||||||
|
def remote_match(name):
|
||||||
|
"""Find all remote repositories matching the given name."""
|
||||||
|
return [ r for r in gitea.all_remote_repos() if r.name == name ]
|
||||||
|
|
||||||
|
def resolve(name, local_only=False, remote_only=False):
|
||||||
|
assert not (local_only and remote_only)
|
||||||
|
|
||||||
|
# Resolve full names directly
|
||||||
|
if "/" in name:
|
||||||
|
if not remote_only and LocalRepo.exists(name):
|
||||||
|
return LocalRepo(name)
|
||||||
|
if local_only:
|
||||||
|
raise ResolveMissingException(name, local_only, remote_only)
|
||||||
|
|
||||||
|
r = gitea.repo_get(name)
|
||||||
|
if r is None:
|
||||||
|
raise ResolveMissingException(name, local_only, remote_only)
|
||||||
|
return r
|
||||||
|
|
||||||
|
# Match local names without owners
|
||||||
|
if not remote_only:
|
||||||
|
r = local_match(name)
|
||||||
|
if len(r) == 0 and local_only:
|
||||||
|
raise ResolveMissingException(name, local_only, remote_only)
|
||||||
|
elif len(r) == 1:
|
||||||
|
return r[0]
|
||||||
|
elif len(r) > 1:
|
||||||
|
raise ResolveAmbiguousException(name, r, "local")
|
||||||
|
|
||||||
|
# Match remote names without owners
|
||||||
|
if not local_only:
|
||||||
|
r = remote_match(name)
|
||||||
|
if len(r) == 0:
|
||||||
|
raise ResolveMissingException(name, local_only, remote_only)
|
||||||
|
elif len(r) == 1:
|
||||||
|
return r[0]
|
||||||
|
else:
|
||||||
|
raise ResolveAmbiguousException(name, r, "remote")
|
||||||
|
|
||||||
|
#
|
||||||
|
# Utilities
|
||||||
|
#
|
||||||
|
|
||||||
|
def print_repo(r, branches=None, tags=None, has_giteapc=True):
|
||||||
|
color = "ARGYBMW"[sum(map(ord,r.owner[:5])) % 7]
|
||||||
|
|
||||||
|
print(colors()[color] +
|
||||||
|
"{}{_}/{W}{}{_}".format(r.owner, r.name, **colors()), end="")
|
||||||
|
print(" (remote)" if r.remote else " (local)", end="")
|
||||||
|
if r.remote and r.parent:
|
||||||
|
print(" {A}[{}]{_}".format(r.parent.fullname, **colors()), end="")
|
||||||
|
if r.remote and has_giteapc == False:
|
||||||
|
print(" {R}(NOT SUPPORTED){_}".format(**colors()), end="")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
if r.remote:
|
||||||
|
print(("\n" + r.description).replace("\n", "\n ")[1:])
|
||||||
|
else:
|
||||||
|
print(" {W}Path:{_}".format(**colors()), r.folder, end="")
|
||||||
|
if os.path.islink(r.folder):
|
||||||
|
print(" ->", os.readlink(r.folder))
|
||||||
|
else:
|
||||||
|
print("")
|
||||||
|
branches = r.branches()
|
||||||
|
tags = r.tags()
|
||||||
|
|
||||||
|
if branches:
|
||||||
|
print(" {W}Branches:{_}".format(**colors()), end="")
|
||||||
|
for b in branches:
|
||||||
|
if "local" in b and b["local"] == False:
|
||||||
|
print(" {A}{}{_}".format(b["name"], **colors()), end="")
|
||||||
|
else:
|
||||||
|
print(" " + b["name"], end="")
|
||||||
|
print("")
|
||||||
|
|
||||||
|
if tags:
|
||||||
|
print(" {W}Tags:{_}".format(**colors()),
|
||||||
|
" ".join(t["name"] for t in reversed(tags)))
|
||||||
|
|
||||||
|
def pretty_repo(r):
|
||||||
|
color = "ARGYBMW"[sum(map(ord,r.owner[:5])) % 7]
|
||||||
|
return colors()[color] + "{}{_}/{W}{}{_}".format(r.owner,r.name,**colors())
|
||||||
|
|
||||||
|
def split_config(name):
|
||||||
|
"""Splits REPOSITORY[@VERSION][:CONFIGURATION] into components."""
|
||||||
|
RE_CONFIG = re.compile(r'^([^@:]+)(?:@([^@:]+))?(?:[:]([^@:]+))?')
|
||||||
|
m = re.match(RE_CONFIG, name)
|
||||||
|
if m is None:
|
||||||
|
return None
|
||||||
|
|
||||||
|
repo, version, config = m[1], m[2] or "", m[3] or ""
|
||||||
|
return repo, version, config
|
||||||
|
|
||||||
|
#
|
||||||
|
# repo list command
|
||||||
|
#
|
||||||
|
|
||||||
|
def list(*args, remote=False):
|
||||||
|
if len(args) > 1:
|
||||||
|
return fatal("repo list: too many arguments")
|
||||||
|
|
||||||
|
if remote:
|
||||||
|
# Since there aren't many repositories under [giteapc], just list them
|
||||||
|
# and then filter by hand (this avoids some requests and makes search
|
||||||
|
# results more consistent with local repositories)
|
||||||
|
repos = gitea.all_remote_repos()
|
||||||
|
else:
|
||||||
|
repos = LocalRepo.all()
|
||||||
|
|
||||||
|
# - Ignore case in pattern
|
||||||
|
# - Add '*' at start and end to match substrings only
|
||||||
|
pattern = args[0].lower() if args else "*"
|
||||||
|
if not pattern.startswith("*"):
|
||||||
|
pattern = "*" + pattern
|
||||||
|
if not pattern.endswith("*"):
|
||||||
|
pattern = pattern + "*"
|
||||||
|
|
||||||
|
# Filter
|
||||||
|
repos = [ r for r in repos
|
||||||
|
if fnmatch.fnmatch(r.fullname.lower(), pattern)
|
||||||
|
or r.remote and fnmatch.fnmatch(r.description.lower(), pattern) ]
|
||||||
|
|
||||||
|
# Print
|
||||||
|
if repos == []:
|
||||||
|
if args:
|
||||||
|
print(f"no repository matching '{args[0]}'")
|
||||||
|
else:
|
||||||
|
print(f"no repository")
|
||||||
|
return 1
|
||||||
|
else:
|
||||||
|
for r in repos:
|
||||||
|
print_repo(r)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
#
|
||||||
|
# repo fetch command
|
||||||
|
#
|
||||||
|
|
||||||
|
def fetch(*args, use_ssh=False, use_https=False, force=False, update=False):
|
||||||
|
# Use HTTPS by default
|
||||||
|
if use_ssh and use_https:
|
||||||
|
return fatal("repo fetch: --ssh and --https are mutually exclusive")
|
||||||
|
protocol = "ssh" if use_ssh else "https"
|
||||||
|
|
||||||
|
# With no arguments, fetch all local repositories
|
||||||
|
if args == ():
|
||||||
|
for r in LocalRepo.all():
|
||||||
|
msg(f"Fetching {pretty_repo(r)}...")
|
||||||
|
r.fetch()
|
||||||
|
if update:
|
||||||
|
r.pull()
|
||||||
|
return 0
|
||||||
|
|
||||||
|
for spec in args:
|
||||||
|
name, version, config = split_config(spec)
|
||||||
|
r = resolve(name)
|
||||||
|
|
||||||
|
# If this is a local repository, just git fetch
|
||||||
|
if not r.remote:
|
||||||
|
if version:
|
||||||
|
msg("Checking out {W}{}{_}".format(version, **colors()))
|
||||||
|
r.checkout(version)
|
||||||
|
|
||||||
|
msg(f"Fetching {pretty_repo(r)}...")
|
||||||
|
r.fetch()
|
||||||
|
if update:
|
||||||
|
r.pull()
|
||||||
|
continue
|
||||||
|
|
||||||
|
msg(f"Cloning {pretty_repo(r)}...")
|
||||||
|
|
||||||
|
# For remote repositories, make sure the repository supports GiteaPC
|
||||||
|
has_tag = "giteapc" in gitea.repo_topics(r)
|
||||||
|
|
||||||
|
if has_tag or force:
|
||||||
|
LocalRepo.clone(r, protocol)
|
||||||
|
if not has_tag and force:
|
||||||
|
warn(f"{r.fullname} doesn't have the [giteapc] tag")
|
||||||
|
if not has_tag and not force:
|
||||||
|
fatal(f"{r.fullname} doesn't have the [giteapc] tag, use -f to force")
|
||||||
|
|
||||||
|
#
|
||||||
|
# repo show command
|
||||||
|
#
|
||||||
|
|
||||||
|
def show(*args, remote=False, path=False):
|
||||||
|
if remote and path:
|
||||||
|
raise Error("repo show: -r and -p are exclusive")
|
||||||
|
|
||||||
|
if not remote:
|
||||||
|
for name in args:
|
||||||
|
r = resolve(name, local_only=True)
|
||||||
|
if path:
|
||||||
|
print(LocalRepo.path(r.fullname))
|
||||||
|
else:
|
||||||
|
print_repo(r)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
repos = []
|
||||||
|
for name in args:
|
||||||
|
r = resolve(name, remote_only=True)
|
||||||
|
branches = gitea.repo_branches(r)
|
||||||
|
tags = gitea.repo_tags(r)
|
||||||
|
topics = gitea.repo_topics(r)
|
||||||
|
repos.append((r, branches, tags, "giteapc" in topics))
|
||||||
|
|
||||||
|
for (r, branches, tags, has_giteapc) in repos:
|
||||||
|
print_repo(r, branches, tags, has_giteapc=has_giteapc)
|
||||||
|
|
||||||
|
#
|
||||||
|
# repo build command
|
||||||
|
#
|
||||||
|
|
||||||
|
def build(*args, install=False, skip_configure=False, update=False):
|
||||||
|
if len(args) < 1:
|
||||||
|
return fatal("repo build: specify at least one repository")
|
||||||
|
|
||||||
|
specs = []
|
||||||
|
for spec in args:
|
||||||
|
repo, version, config = split_config(spec)
|
||||||
|
repo = resolve(repo, local_only=True)
|
||||||
|
specs.append((repo, version, config))
|
||||||
|
|
||||||
|
msg("Will build:", ", ".join(pretty_repo(spec[0]) for spec in specs))
|
||||||
|
|
||||||
|
for (r, version, config) in specs:
|
||||||
|
pretty = pretty_repo(r)
|
||||||
|
config_string = f" for {config}" if config else ""
|
||||||
|
|
||||||
|
if version != "":
|
||||||
|
msg("{}: Checking out {W}{}{_}".format(pretty, version, **colors()))
|
||||||
|
r.checkout(version)
|
||||||
|
if update:
|
||||||
|
r.pull()
|
||||||
|
|
||||||
|
# Check that the project has a Makefile
|
||||||
|
if not os.path.exists(r.makefile):
|
||||||
|
raise Error(f"{r.fullname} has no giteapc.make")
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
if config:
|
||||||
|
env["GITEAPC_CONFIG"] = config
|
||||||
|
env["GITEAPC_PREFIX"] = PREFIX_FOLDER
|
||||||
|
|
||||||
|
if not skip_configure:
|
||||||
|
msg(f"{pretty}: Configuring{config_string}")
|
||||||
|
r.make("configure", env)
|
||||||
|
|
||||||
|
msg(f"{pretty}: Building")
|
||||||
|
r.make("build", env)
|
||||||
|
|
||||||
|
if install:
|
||||||
|
msg(f"{pretty}: Installing")
|
||||||
|
r.make("install", env)
|
||||||
|
|
||||||
|
msg(f"{pretty}: Done! :D")
|
||||||
|
|
||||||
|
|
||||||
|
#
|
||||||
|
# repo install command
|
||||||
|
#
|
||||||
|
|
||||||
|
def install(*args, use_https=False, use_ssh=False, update=False):
|
||||||
|
if args == ():
|
||||||
|
return 0
|
||||||
|
|
||||||
|
# First download every repository, and only then build
|
||||||
|
fetch(*args, use_https=use_https, use_ssh=use_ssh)
|
||||||
|
build(*args, install=True, update=update)
|
||||||
|
|
||||||
|
#
|
||||||
|
# repo uninstall command
|
||||||
|
#
|
||||||
|
|
||||||
|
def uninstall(*args, keep=False):
|
||||||
|
if len(args) < 1:
|
||||||
|
return fatal("repo uninstall: specify at least one repository")
|
||||||
|
|
||||||
|
for name in args:
|
||||||
|
r = resolve(name, local_only=True)
|
||||||
|
msg(f"{pretty_repo(r)}: Uninstalling")
|
||||||
|
|
||||||
|
env = os.environ.copy()
|
||||||
|
env["GITEAPC_PREFIX"] = PREFIX_FOLDER
|
||||||
|
r.make("uninstall", env)
|
||||||
|
|
||||||
|
if not keep:
|
||||||
|
msg("{}: {R}Removing files{_}".format(pretty_repo(r), **colors()))
|
||||||
|
|
||||||
|
if os.path.isdir(r.folder):
|
||||||
|
shutil.rmtree(r.folder)
|
||||||
|
elif os.path.islink(r.folder):
|
||||||
|
os.remove(r.folder)
|
||||||
|
else:
|
||||||
|
raise Error(f"cannot handle {r.folder} (not a folder/symlink)")
|
||||||
|
|
||||||
|
parent = os.path.dirname(r.folder)
|
||||||
|
if not os.listdir(parent):
|
||||||
|
os.rmdir(parent)
|
136
giteapc/repos.py
Normal file
136
giteapc/repos.py
Normal file
|
@ -0,0 +1,136 @@
|
||||||
|
from giteapc.config import REPO_FOLDER
|
||||||
|
from giteapc.util import *
|
||||||
|
import subprocess
|
||||||
|
from subprocess import PIPE
|
||||||
|
import os.path
|
||||||
|
import shutil
|
||||||
|
import glob
|
||||||
|
|
||||||
|
class RemoteRepo:
|
||||||
|
# Create a remote repo from the JSON object returned by Gitea
|
||||||
|
def __init__(self, j):
|
||||||
|
self.j = j
|
||||||
|
self.remote = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
return self.j["name"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def owner(self):
|
||||||
|
return self.j["owner"]["username"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def fullname(self):
|
||||||
|
return self.j["full_name"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def description(self):
|
||||||
|
return self.j["description"]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def parent(self):
|
||||||
|
p = self.j["parent"]
|
||||||
|
return RemoteRepo(p) if p is not None else None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def url(self):
|
||||||
|
return f"/repos/{self.owner}/{self.name}"
|
||||||
|
|
||||||
|
def clone_url(self, protocol):
|
||||||
|
if protocol == "ssh":
|
||||||
|
return self.j["ssh_url"]
|
||||||
|
else:
|
||||||
|
return self.j["clone_url"]
|
||||||
|
|
||||||
|
|
||||||
|
class LocalRepo:
|
||||||
|
# Create a remote repo from the full name or path
|
||||||
|
def __init__(self, fullname):
|
||||||
|
if fullname.startswith(REPO_FOLDER + "/"):
|
||||||
|
fullname = fullname[len(REPO_FOLDER)+1:]
|
||||||
|
|
||||||
|
assert fullname.count("/") == 1
|
||||||
|
|
||||||
|
self.fullname = fullname
|
||||||
|
self.owner, self.name = fullname.split("/")
|
||||||
|
self.folder = REPO_FOLDER + "/" + fullname
|
||||||
|
self.remote = False
|
||||||
|
self.makefile = self.folder + "/giteapc.make"
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def path(fullname):
|
||||||
|
return REPO_FOLDER + "/" + fullname
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def exists(fullname):
|
||||||
|
return os.path.exists(LocalRepo.path(fullname))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def all():
|
||||||
|
return [ LocalRepo(path) for path in glob.glob(REPO_FOLDER + f"/*/*") ]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def clone(r, method="https"):
|
||||||
|
src = r.clone_url(method)
|
||||||
|
dst = LocalRepo.path(r.fullname)
|
||||||
|
mkdir_p(dst)
|
||||||
|
cmd = ["git", "clone", src, dst]
|
||||||
|
|
||||||
|
try:
|
||||||
|
run(cmd, stdout=PIPE, stderr=PIPE)
|
||||||
|
return LocalRepo(r.fullname)
|
||||||
|
except ProcessError as e:
|
||||||
|
# On error, delete the failed clone
|
||||||
|
shutil.rmtree(dst)
|
||||||
|
raise e
|
||||||
|
|
||||||
|
# Git commands
|
||||||
|
|
||||||
|
def _git(self, command, *args, **kwargs):
|
||||||
|
return run(["git", "-C", self.folder] + command,
|
||||||
|
*args, **kwargs)
|
||||||
|
|
||||||
|
def is_on_branch(self):
|
||||||
|
try:
|
||||||
|
self._git(["symbolic-ref", "-q", "HEAD"])
|
||||||
|
return True
|
||||||
|
except ProcessError as e:
|
||||||
|
if e.returncode == 1:
|
||||||
|
return False
|
||||||
|
raise e
|
||||||
|
|
||||||
|
def fetch(self):
|
||||||
|
self._git(["fetch"])
|
||||||
|
|
||||||
|
def pull(self):
|
||||||
|
if self.is_on_branch():
|
||||||
|
self._git(["pull"])
|
||||||
|
|
||||||
|
def checkout(self, version):
|
||||||
|
self._git(["checkout", version], stdout=PIPE, stderr=PIPE)
|
||||||
|
|
||||||
|
def branches(self):
|
||||||
|
proc = self._git(["branch"], stdout=subprocess.PIPE)
|
||||||
|
local = proc.stdout.decode("utf-8").split("\n")
|
||||||
|
local = [ b[2:] for b in local if b ]
|
||||||
|
|
||||||
|
proc = self._git(["branch", "-r"], stdout=subprocess.PIPE)
|
||||||
|
remote = proc.stdout.decode("utf-8").split("\n")
|
||||||
|
remote = [ b[9:] for b in remote
|
||||||
|
if b and b.startswith(" origin/") and "/HEAD " not in b ]
|
||||||
|
remote = [ b for b in remote if b not in local ]
|
||||||
|
|
||||||
|
return [ {"name": b, "local": True } for b in local ] + \
|
||||||
|
[ {"name": b, "local": False} for b in remote ]
|
||||||
|
|
||||||
|
def tags(self):
|
||||||
|
proc = self._git(["tag", "--list"], stdout=subprocess.PIPE)
|
||||||
|
tags = proc.stdout.decode("utf-8").split("\n")
|
||||||
|
return [ {"name": t} for t in tags if t ]
|
||||||
|
|
||||||
|
# Make commands
|
||||||
|
|
||||||
|
def make(self, target, env=None):
|
||||||
|
with ChangeDirectory(self.folder):
|
||||||
|
return run(["make", "-f", "giteapc.make", target], env=env)
|
156
giteapc/util.py
Normal file
156
giteapc/util.py
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
import os.path
|
||||||
|
import shlex
|
||||||
|
|
||||||
|
# Error management; the base Error class is provided for errors to be caught at
|
||||||
|
# top-level and displayed as a GiteaPC error message.
|
||||||
|
|
||||||
|
class Error(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
class NetworkError(Error):
|
||||||
|
def __init__(self, status):
|
||||||
|
self.status = status
|
||||||
|
|
||||||
|
class ProcessError(Error):
|
||||||
|
def __init__(self, process):
|
||||||
|
self.process = process
|
||||||
|
self.returncode = process.returncode
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
p = self.process
|
||||||
|
e = f"error {p.returncode} in command: "
|
||||||
|
c = " ".join(shlex.quote(arg) for arg in p.args)
|
||||||
|
e += "{W}{}{_}\n".format(c, **colors())
|
||||||
|
|
||||||
|
out = p.stdout.decode('utf-8').strip() if p.stdout else ""
|
||||||
|
err = p.stderr.decode('utf-8').strip() if p.stderr else ""
|
||||||
|
|
||||||
|
# Show standard output and standard error (omit names if only one has
|
||||||
|
# content to show)
|
||||||
|
if len(out) > 0 and len(err) > 0:
|
||||||
|
e += "{W}Standard output:{_}\n".format(**colors())
|
||||||
|
if len(out) > 0:
|
||||||
|
e += out + "\n"
|
||||||
|
if len(out) > 0 and len(err) > 0:
|
||||||
|
e += "{W}Standard error:{_}\n".format(**colors())
|
||||||
|
if len(err) > 0:
|
||||||
|
e += err + "\n"
|
||||||
|
|
||||||
|
return e.strip()
|
||||||
|
|
||||||
|
class ResolveMissingException(Error):
|
||||||
|
def __init__(self, name, local_only, remote_only):
|
||||||
|
self.name = name
|
||||||
|
self.local_only = local_only
|
||||||
|
self.remote_only = remote_only
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
if self.local_only:
|
||||||
|
spec = "local repository"
|
||||||
|
elif self.remote_only:
|
||||||
|
spec = "remote repository"
|
||||||
|
else:
|
||||||
|
spec = "local or remote repository"
|
||||||
|
|
||||||
|
return f"no such {spec}: '{self.name}'"
|
||||||
|
|
||||||
|
class ResolveAmbiguousException(Error):
|
||||||
|
def __init__(self, name, matches, kind):
|
||||||
|
self.name = name
|
||||||
|
self.matches = [r.fullname for r in matches]
|
||||||
|
self.kind = kind
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return f"multiple {self.kind} repositories match '{self.name}': " + \
|
||||||
|
", ".join(self.matches)
|
||||||
|
|
||||||
|
def warn(*args):
|
||||||
|
print("{Y}warning:{_}".format(**colors()), *args, file=sys.stderr)
|
||||||
|
def fatal(*args):
|
||||||
|
print("{R}error:{_}".format(**colors()), *args, file=sys.stderr)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
# Color output
|
||||||
|
|
||||||
|
def colors():
|
||||||
|
colors = {
|
||||||
|
# Gray, Red, Green, Yello, Blue, Magenta, Cyan, White
|
||||||
|
"A": "30;1", "R": "31;1", "G": "32;1", "Y": "33;1",
|
||||||
|
"B": "34;1", "M": "35;1", "C": "36;1", "W": "37;1",
|
||||||
|
# Same but without bold
|
||||||
|
"a": "30", "r": "31", "g": "32", "y": "33",
|
||||||
|
"b": "34", "m": "35", "c": "36", "w": "37",
|
||||||
|
# Italic
|
||||||
|
"i": "3",
|
||||||
|
# Clear formatting
|
||||||
|
"_": "0",
|
||||||
|
}
|
||||||
|
# Disable colors if stdout is not a TTY
|
||||||
|
if sys.stdout.isatty():
|
||||||
|
return { name: f"\x1b[{code}m" for name, code in colors.items() }
|
||||||
|
else:
|
||||||
|
return { name: "" for name in colors }
|
||||||
|
|
||||||
|
def msg(*args, **kwargs):
|
||||||
|
print("{c}<giteapc>{_}".format(**colors()), *args, **kwargs)
|
||||||
|
|
||||||
|
# Change directory and guarantee changing back even if an exception occurs
|
||||||
|
|
||||||
|
class ChangeDirectory:
|
||||||
|
def __init__(self, destination):
|
||||||
|
self.destination = destination
|
||||||
|
|
||||||
|
def __enter__(self):
|
||||||
|
self.source = os.getcwd()
|
||||||
|
os.chdir(self.destination)
|
||||||
|
|
||||||
|
def __exit__(self, type, value, traceback):
|
||||||
|
os.chdir(self.source)
|
||||||
|
|
||||||
|
# Tool detection
|
||||||
|
|
||||||
|
def has_curl():
|
||||||
|
proc = subprocess.run(["curl", "--version"], stdout=subprocess.DEVNULL)
|
||||||
|
return proc.returncode == 0
|
||||||
|
|
||||||
|
def has_git():
|
||||||
|
proc = subprocess.run(["git", "--version"], stdout=subprocess.DEVNULL)
|
||||||
|
return proc.returncode == 0
|
||||||
|
|
||||||
|
# HTTP requests
|
||||||
|
|
||||||
|
def requests_get(url, params):
|
||||||
|
# Don't import requests until the gitea module confirms it's available
|
||||||
|
import requests
|
||||||
|
r = requests.get(url, params)
|
||||||
|
if r.status_code != 200:
|
||||||
|
raise NetworkError(r.status_code)
|
||||||
|
return r.text
|
||||||
|
|
||||||
|
def curl_get(url, params):
|
||||||
|
if params:
|
||||||
|
url = url + "?" + "&".join(f"{n}={v}" for (n,v) in params.items())
|
||||||
|
proc = subprocess.run(["curl", "-s", url], stdout=subprocess.PIPE)
|
||||||
|
assert proc.returncode == 0
|
||||||
|
return proc.stdout.decode("utf-8")
|
||||||
|
|
||||||
|
# Create path to folder
|
||||||
|
|
||||||
|
def mkdir_p(folder):
|
||||||
|
try:
|
||||||
|
os.mkdir(folder)
|
||||||
|
except FileExistsError:
|
||||||
|
return
|
||||||
|
except FileNotFoundError:
|
||||||
|
mkdir_p(os.path.dirname(folder))
|
||||||
|
os.mkdir(folder)
|
||||||
|
|
||||||
|
# Run process and throw exception on error
|
||||||
|
|
||||||
|
def run(process, *args, **kwargs):
|
||||||
|
proc = subprocess.run(process, *args, **kwargs)
|
||||||
|
if proc.returncode != 0:
|
||||||
|
raise ProcessError(proc)
|
||||||
|
return proc
|
56
install.sh
Executable file
56
install.sh
Executable file
|
@ -0,0 +1,56 @@
|
||||||
|
#! /usr/bin/env bash
|
||||||
|
|
||||||
|
TAG=$(printf "\x1b[36m<giteapc>\x1b[0m")
|
||||||
|
PREFIX=${GITEAPC_PREFIX:-$HOME/.local}
|
||||||
|
URL="https://gitea.planet-casio.com/Lephenixnoir/giteapc/archive/master.tar.gz"
|
||||||
|
|
||||||
|
# Download the source code
|
||||||
|
|
||||||
|
cd $(mktemp)
|
||||||
|
curl $URL -o giteapc-master.tar.gz
|
||||||
|
tar -xzf giteapc-master.tar.gz && cd giteapc
|
||||||
|
|
||||||
|
# Install the program itself (not to $PREFIX, which is for programs installed
|
||||||
|
# by GiteaPC)
|
||||||
|
|
||||||
|
make install
|
||||||
|
|
||||||
|
# Check whether the bin folder is already in the PATH
|
||||||
|
|
||||||
|
if [[ ":$PATH:" =~ ":$PREFIX/bin:" ]]; then
|
||||||
|
echo "$TAG $PREFIX/bin is already in your PATH, we're good to go!"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Try to find a suitable startup file to extend the PATH in
|
||||||
|
|
||||||
|
default="$HOME/.profile"
|
||||||
|
candidates=".bashrc .zshrc .bash_profile .profile .zprofile"
|
||||||
|
|
||||||
|
for c in $candidates; do
|
||||||
|
[[ -f "$HOME/$c" ]] && default="$HOME/$c"
|
||||||
|
done
|
||||||
|
|
||||||
|
# Suggest to add the path to binaries to the PATH at startup
|
||||||
|
|
||||||
|
cat <<EOF
|
||||||
|
$TAG In order to use programs installed by GiteaPC, you will need to add their
|
||||||
|
$TAG install folder to your PATH. This can be done automatically when you log
|
||||||
|
$TAG in by adding the following command to your startup file:
|
||||||
|
$TAG
|
||||||
|
$TAG export PATH="\$PATH:$PREFIX/bin"
|
||||||
|
$TAG
|
||||||
|
$TAG -> Press Enter to add this command to $default, or
|
||||||
|
$TAG -> Type another file name to add this command to, or
|
||||||
|
$TAG -> Type "-" to skip setting the PATH entirely.
|
||||||
|
EOF
|
||||||
|
|
||||||
|
read -p "> " startup_file
|
||||||
|
[[ -z "$startup_file" ]] && startup_file=$default
|
||||||
|
|
||||||
|
if [[ "$startup_file" == "-" ]]; then
|
||||||
|
echo "$TAG Skipped setting the PATH."
|
||||||
|
else
|
||||||
|
echo "export PATH=\"\$PATH:$PREFIX/bin\"" >> $startup_file
|
||||||
|
echo "$TAG Set the PATH in $startup_file, this will take effect next login."
|
||||||
|
fi
|
Loading…
Reference in a new issue