diff --git a/TODO.md b/TODO.md index 48aa81e..1b08809 100644 --- a/TODO.md +++ b/TODO.md @@ -36,6 +36,7 @@ - [ ] existence of `.local`, `.local/programs`, `.local/bin` - [ ] that permissions are ok - [ ] that `.local/bin` are in `PATH` + - [ ] UI, add some progress to install - [ ] allow to list versions - [ ] allow to force installation of a given version - [-] improve search algorithm, fuzzing diff --git a/cliget.py b/cliget.py index d27ea0a..728581e 100644 --- a/cliget.py +++ b/cliget.py @@ -1,15 +1,17 @@ """Cliget - install cli tools in you user profile Usage: - cliget [-v] [-c URL] list cliget [-v] [-c URL] search PAT + cliget [-v] [-c URL] info TOOL + cliget [-v] [-c URL] list cliget [-v] [-c URL] versions TOOLS ... cliget [-v] [-c URL] allversions TOOL cliget [-v] [-c URL] install TOOLS ... cliget [-v] [-c URL] update [--all] [TOOLS ...] -list list all installed tools search search catalog for tools with the given pattern +info show details of an entry in the catalog +list list all installed tools versions TOOLS list current and latest versions of some tools allversions TOOL list all versions of a tool install TOOLS install some tools @@ -25,7 +27,7 @@ Options: """ import logging -import sys, os +import sys, os, shutil from docopt import docopt from yaml import load, SafeLoader from semver import VersionInfo @@ -39,7 +41,7 @@ def trace(*mess): print("TRACE", mess) def warn(*mess): - print(f"WARN {mess[0]}") + print("WARN", mess) class DotDict(dict): def __getattr__(self, name): @@ -61,12 +63,12 @@ def _find_semver(s:str) -> VersionInfo: except Exception as e: trace("parse error", e) return ver - def _local_version(cmd): ver = VersionInfo(0) try: first_line = run([cmd, '--version'], input='', text=True, capture_output=True, check=True, timeout=0.1).stdout.split('\n')[0] + trace(cmd, '=>', first_line) ver = _find_semver(first_line) except Exception as e: trace("run error", e) @@ -92,6 +94,16 @@ def dolist(options): for (cli, _, ver) in _internal_list(options): print(cli, ver) +def doinfo(options): + tool = options.TOOL + ctl = _load_catalog(options) + if tool in ctl: + print(tool) + for (k,v) in ctl[tool].items(): + print(f' {k}: {v}') + else: + warn(f'{tool} not in catalog') + def dosearch(options): pat = options.PAT ctl = _load_catalog(options) @@ -100,7 +112,7 @@ def dosearch(options): trace(cli, props) rtitle = fuzz.ratio(cli, pat) rdesc = fuzz.partial_token_set_ratio(props['desc'], pat) - score = 0.2 * rtitle + rdesc + score = rtitle + rdesc if rtitle>60 else rdesc L.append((cli, props['desc'], score)) L = sorted(L, key=lambda x: -x[-1]) # TODO format a as table @@ -114,7 +126,6 @@ def dolistupdate(options): pass pass - def _gh_versions(repo:str) -> [VersionInfo|None]: [owner, repo] = repo.split("/") url = f'https://api.github.com/repos/{owner}/{repo}/releases' @@ -140,7 +151,7 @@ def doversions(options): print(f"{cli} | {lver} | {rver}") else: warn(f'{tool} not in catalog') - + def doallversions(options): tool = options.TOOL ctl = _load_catalog(options) @@ -152,7 +163,6 @@ def doallversions(options): print("\n".join(vers)) else: warn(f'{tool} not in catalog') - def doinstall(options): tools = options.TOOLS @@ -161,19 +171,53 @@ def doinstall(options): if tool in ctl: props = ctl[tool] if props.github: - vers = _gh_version(props.github) - trace(vers) + rver = _gh_version(props.github) + lver = _local_version(tool) + trace(lver,rver) + if rver > lver: + _perform_install(tool, props.github) else: warn(f'{tool} not in catalog') - -def perform_install(cli, repo): - # get arch and os - # dl asset - # mkdirs - # unpack - # symlink - +def _match_arch_machine(name:str) -> bool: + sysname = os.uname().sysname.lower() # os + machine = os.uname().machine.lower() # arch + return name.lower().find(sysname)>0 and name.lower().find(machine)>0 + +def _perform_install(cli, repo, version=None): + # get asset list + [owner, repo] = repo.split("/") + url = f'https://api.github.com/repos/{owner}/{repo}/releases/latest' + r = requests.get(url) + assets = r.json()['assets'] + trace(assets) + # select right asset + asset = DotDict(next(filter(lambda x: _match_arch_machine(x['name']),assets))) + trace(asset.name, asset.url) + # mkdirs + p = os.path + home = os.environ["HOME"] + assetshome = p.join(home, '.cache/cliget/assets') + os.makedirs(assetshome, exist_ok=True) + location = p.join(assetshome, asset.name) + trace(f"will dl {location}") + # dl asset if not already there + if not p.exists(location): + dlurl = requests.get(asset.url).json()['browser_download_url'] + r = requests.get(dlurl, allow_redirects=True, stream=True) + with open(location, 'wb') as fd: + shutil.copyfileobj(r.raw, fd) + trace("downloaded") + # unpack asset + if not asset.name.endswith('.tar.gz'): + raise ValueError('package type not handled') + progloc = p.join(home, '.local/program/'+cli) + trace("process tgz") + os.makedirs(progloc, exist_ok=True) + run(["tar", "xfz", location, "-C", progloc]) + # symlink + os.symlink(p.join(progloc, cli), p.join(home, '.local/bin', cli)) + if __name__ == '__main__': if "DEBUG" in os.environ: logging.basicConfig(level=logging.DEBUG) @@ -183,13 +227,15 @@ if __name__ == '__main__': options = docopt(__doc__, version='Cliget 0.1.0') options = DotDict({k.replace('-','_'):v for (k,v) in options.items() if v is not None}) trace(options) - if options.list: + if options.info: + doinfo(options) + elif options.list: dolist(options) elif options.search: dosearch(options) elif options.versions: doversions(options) - elif options.allversions(options): + elif options.allversions: doallversions(options) elif options.install or options.update: if not options.__all and len(options.TOOLS)==0: