diff --git a/giteapc.py b/giteapc.py old mode 100644 new mode 100755 index 122a5ab..f208c75 --- a/giteapc.py +++ b/giteapc.py @@ -22,11 +22,8 @@ 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 +# * build, install: test repository update logic +# * install: test dependency logic usage_string = """ usage: {R}giteapc{_} [{R}list{_}|{R}fetch{_}|{R}show{_}|{R}build{_}|{R}install{_}|{R}uninstall{_}] [{g}{i}ARGS...{_}] @@ -37,26 +34,28 @@ 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 + List all repositories on this computer. With -r, list 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. + Clone, fetch, or pull a repository. If no repository is specified, fetches + all 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. + Show 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 + Configure and build a local repository. A specific configuration can be + requested. With -i, also install if build is successful. --skip-configure + builds without configuring (useful for rebuilds). With -u, pull 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 install{_} [{R}--https{_}|{R}--ssh{_}] [{R}-u{_}] [{R}-y{_}] {g}{i}REPOSITORY{_}[{R}@{_}{g}{i}VERSION{_}][{R}:{_}{g}{i}CONFIG{_}]{g}{i}...{_} + Fetch repositories and their dependencies, then build and install them. + With -u, pulls local repositories (update mode). With -yes, do not ask for + interactive confirmation. {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. + Uninstall the build products of the specified repositories and remove the + source files. With -k, keep the source files. {W}Important folders{_} diff --git a/giteapc/repo.py b/giteapc/repo.py index 56c5e6c..a426e9f 100644 --- a/giteapc/repo.py +++ b/giteapc/repo.py @@ -73,6 +73,7 @@ def print_repo(r, branches=None, tags=None, has_giteapc=True): if r.remote: print(("\n" + r.description).replace("\n", "\n ")[1:]) else: + print(" {W}HEAD:{_}".format(**colors()), r.describe(all=True)) print(" {W}Path:{_}".format(**colors()), r.folder, end="") if os.path.islink(r.folder): print(" ->", os.readlink(r.folder)) @@ -108,6 +109,11 @@ def split_config(name): repo, version, config = m[1], m[2] or "", m[3] or "" return repo, version, config +def make_config(name, version, config): + version = f"@{version}" if version else "" + config = f":{config}" if config else "" + return name + version + config + # # repo list command # @@ -238,8 +244,6 @@ def build(*args, install=False, skip_configure=False, update=False): 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 "" @@ -247,8 +251,14 @@ def build(*args, install=False, skip_configure=False, update=False): if version != "": msg("{}: Checking out {W}{}{_}".format(pretty, version, **colors())) r.checkout(version) - if update: - r.pull() + if update: + previous = r.describe() + r.pull() + new = r.describe() + if new == previous: + msg("{}: Still at {W}{}{_}, skipping build".format(pretty, + previous, **colors())) + continue # Check that the project has a Makefile if not os.path.exists(r.makefile): @@ -277,13 +287,59 @@ def build(*args, install=False, skip_configure=False, update=False): # repo install command # -def install(*args, use_https=False, use_ssh=False, update=False): +def install(*args, use_https=False, use_ssh=False, update=False, yes=False): if args == (): return 0 + def recursive_fetch(spec, fetched_so_far): + repos_to_build = [] + repo, version, config = split_config(spec) + r = resolve(repo, local_only=True) + + if r.fullname not in fetched_so_far: + fetched_so_far.add(r.fullname) + fetch(repo, use_https=use_https, use_ssh=use_ssh, update=update) + + for dep_spec in r.dependencies(): + rtb, fsf = recursive_fetch(dep_spec, fetched_so_far) + repos_to_build += rtb + fetched_so_far = fetched_so_far.union(fsf) + + return repos_to_build + [(r, version, config)], fetched_so_far + # First download every repository, and only then build - fetch(*args, use_https=use_https, use_ssh=use_ssh) - build(*args, install=True, update=update) + repos_to_build = [] + fetched_so_far = set() + + for spec in args: + rtb, fsf = recursive_fetch(spec, fetched_so_far) + repos_to_build += rtb + fetched_so_far = fetched_so_far.union(fsf) + + # Eliminate duplicates and look for version collisions + rd = dict() + + for (r, version, config) in repos_to_build: + name = r.fullname + if name not in rd: + rd[name] = (version, config) + elif rd[name] != (version, config): + s1 = make_config(name, *rd[name]) + s2 = make_config(name, version, config) + return fatal(f"repo install: cannot install both {s1} and {s2}") + + msg("Will install:", ", ".join(make_config(pretty_repo(r), version, config) + for (r, version, config) in repos_to_build)) + + if not yes: + msg("Is that okay (Y/n)? ", end="") + confirm = input().strip() + if confirm not in ["Y", "y", ""]: + return 1 + + for (r, version, config) in repos_to_build: + build(make_config(r.fullname, version, config), install=True, + update=update) # # repo uninstall command diff --git a/giteapc/repos.py b/giteapc/repos.py index 02cc1d3..a5f98cd 100644 --- a/giteapc/repos.py +++ b/giteapc/repos.py @@ -5,6 +5,7 @@ from subprocess import PIPE import os.path import shutil import glob +import re class RemoteRepo: # Create a remote repo from the JSON object returned by Gitea @@ -93,7 +94,7 @@ class LocalRepo: def is_on_branch(self): try: - self._git(["symbolic-ref", "-q", "HEAD"]) + self._git(["symbolic-ref", "-q", "HEAD"], stdout=PIPE) return True except ProcessError as e: if e.returncode == 1: @@ -107,6 +108,11 @@ class LocalRepo: if self.is_on_branch(): self._git(["pull"]) + def describe(self, tags=True, all=False, always=True): + args = ["--tags"]*tags + ["--all"]*all + ["--always"]*always + p = self._git(["describe"] + args, stdout=PIPE) + return p.stdout.decode("utf-8").strip() + def checkout(self, version): self._git(["checkout", version], stdout=PIPE, stderr=PIPE) @@ -134,3 +140,33 @@ class LocalRepo: def make(self, target, env=None): with ChangeDirectory(self.folder): return run(["make", "-f", "giteapc.make", target], env=env) + + # Metadata + + def metadata(self): + RE_METADATA = re.compile(r'^\s*#\s*giteapc\s*:\s*(.*)$') + metadata = dict() + + with open(self.folder + "/giteapc.make", "r") as fp: + makefile = fp.read() + + for line in makefile.split("\n"): + m = re.match(RE_METADATA, line) + if m is None: + break + + for assignment in m[1].split(): + name, value = assignment.split("=", 1) + if name not in metadata: + metadata[name] = value + else: + metadata[name] += "," + value + + return metadata + + def dependencies(self): + metadata = self.metadata() + if "depends" in metadata: + return metadata["depends"].split(",") + else: + return [] diff --git a/giteapc/util.py b/giteapc/util.py index 9d34552..d35ef91 100644 --- a/giteapc/util.py +++ b/giteapc/util.py @@ -133,7 +133,8 @@ 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 + if proc.returncode != 0: + raise NetworkError(-1) return proc.stdout.decode("utf-8") # Create path to folder