diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..1b823a2c19a3eabf1719d24b74771549a44a232e --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +dist +build diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 07d3af5871d86928bd7f7024483f56113115e2d7..4be1cfe048bf1ee528006997c67698300fd076eb 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,5 +1,6 @@ stages: - test + - build variables: PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip" @@ -17,3 +18,47 @@ test: - pytest tags: - docker + +build-linux: + stage: build + image: python:3.6 + before_script: + - pip install .[dev] + script: + - pyinstaller --clean -y cli.spec --distpath . + - echo "Testing the generated CLI works…" && ./funkwhale --help && echo "funkwhale CLI working \o/" + artifacts: + name: "linux_${CI_COMMIT_REF_NAME}" + paths: + - funkwhale + only: + - tags@funkwhale/cli + - master@funkwhale/cli + + tags: + - docker + +build-windows: + # there is a weird Gitlab / windows interaction + # cf https://github.com/cdrx/docker-pyinstaller/issues/38 + # so we cannot use the regular docker executor + stage: build + image: docker:stable + tags: + - docker-build + + variables: + # CI_DEBUG_TRACE: "true" + script: + - docker run --rm -v "$(pwd):/src/" cdrx/pyinstaller-windows:python3 "pip install -r requirements-dev.txt && pyinstaller --clean -y cli.spec --distpath ." + - docker run --rm -v "$(pwd):/src/" cdrx/pyinstaller-windows:python3 "echo 'Testing the generated CLI works…' && wine ./funkwhale.exe --help && echo 'funkwhale CLI working \o/'" + artifacts: + name: "linux_${CI_COMMIT_REF_NAME}" + paths: + - funkwhale.exe + only: + - tags@funkwhale/cli + - master@funkwhale/cli + + tags: + - docker-build diff --git a/README.md b/README.md index dddb1640a3ffdbff6318fd38b18bfb9b7cf6327c..ff6fb206aeb0a626b9a1216aa516a4e5de499e3b 100644 --- a/README.md +++ b/README.md @@ -2,12 +2,39 @@ A command line interface to interact with Funkwhale servers. # Installation +We provide some prebuilt binaries for Windows and Linux. + +On Linux: + +``` +curl -L "https://dev.funkwhale.audio/funkwhale/cli/-/jobs/artifacts/master/raw/funkwhale?job=build-linux" -o /usr/local/bin/funkwhale +chmod +x /usr/local/bin/funkwhale +``` + +On Windows: + +``` +curl -L "https://dev.funkwhale.audio/funkwhale/cli/-/jobs/artifacts/master/raw/funkwhale.exe?job=build-windows" -o funkwhale.exe +``` + +# Usage + +``funkwhale --help`` + +# Installation (from source) + This cli requires python 3.6 or greater: git clone https://dev.funkwhale.audio/funkwhale/cli.git cd cli pip install . -# Usage -``funkwhale --help`` +# Build the binary + +You can build the binarie for you platform using the following commands: + + pip install .[dev] + pyinstaller cli.spec + +This will output a binary in `./dist/funkwhale`. diff --git a/cli.spec b/cli.spec new file mode 100644 index 0000000000000000000000000000000000000000..3116b1c844458af93f54a78717d930d394d59264 --- /dev/null +++ b/cli.spec @@ -0,0 +1,35 @@ +# -*- mode: python -*- + +block_cipher = None + + +a = Analysis( + ["funkwhale_cli/cli.py"], + pathex=["/home/eliotberriot/projects/funkwhale/cli"], + binaries=[], + datas=[], + hiddenimports=[], + hookspath=[], + runtime_hooks=[], + excludes=[], + win_no_prefer_redirects=False, + win_private_assemblies=False, + cipher=block_cipher, + noarchive=False, +) +pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) +exe = EXE( + pyz, + a.scripts, + a.binaries, + a.zipfiles, + a.datas, + [], + name="funkwhale", + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=True, + runtime_tmpdir=None, + console=True, +) diff --git a/funkwhale_cli/cli.py b/funkwhale_cli/cli.py index fa892e39452385d8c852b857925d93a4a7ad6fb2..c3170d0130f80a7ea4d4fdcde6672aee5cd01059 100644 --- a/funkwhale_cli/cli.py +++ b/funkwhale_cli/cli.py @@ -7,6 +7,14 @@ import datetime import dotenv import functools import keyring + +# importing the backends explicitely is required for PyInstaller to work +import keyring.backends.kwallet +import keyring.backends.Windows +import keyring.backends.OS_X +import keyring.backends.SecretService +import keyring.backends.chainer + import logging import math import urllib.parse @@ -16,11 +24,12 @@ import pathvalidate import pathlib import urllib.parse import tqdm -from . import api -from . import config -from . import exceptions -from . import logs -from . import output + +from funkwhale_cli import api +from funkwhale_cli import config +from funkwhale_cli import exceptions +from funkwhale_cli import logs +from funkwhale_cli import output click_log.basic_config(logs.logger) @@ -60,10 +69,18 @@ def async_command(f): _async_reraise = kwargs.pop("_async_reraise", False) try: return loop.run_until_complete(f(*args, **kwargs)) - except (exceptions.FunkwhaleError, aiohttp.client_exceptions.ClientError) as e: + except (aiohttp.client_exceptions.ClientError) as e: + if _async_reraise: + raise + message = str(e) + if hasattr(e, 'status') and e.status == 401: + message = "Remote answered with {}, ensure your are logged in first".format(e.status) + raise click.ClickException(message) + except (exceptions.FunkwhaleError) as e: if _async_reraise: raise - raise click.ClickException(str(e)) + message = str(e) + raise click.ClickException(message) else: raise @@ -118,14 +135,14 @@ class lazy_credential(): return bool(self.value) -def set_server(ctx, url, token): +def set_server(ctx, url, token, use_auth=True): ctx.ensure_object(dict) ctx.obj["SERVER_URL"] = url parsed = urllib.parse.urlparse(url) ctx.obj["SERVER_NETLOC"] = parsed.netloc ctx.obj["SERVER_PROTOCOL"] = parsed.scheme try: - token = token or lazy_credential(url, "_") + token = (token or lazy_credential(url, "_")) if use_auth else None except ValueError as e: raise click.ClickException("Error while retrieving password from keyring: {}. Your password may be incorrect.".format(e.args[0])) except Exception as e: @@ -154,14 +171,26 @@ def set_server(ctx, url, token): default=False, help="Disable logging", ) +@click.option( + "--no-login", + envvar="FUNKWHALE_NO_LOGIN", + is_flag=True, + default=False, + help="Disable authentication/keyring", +) @SERVER_DECORATOR @TOKEN_DECORATOR @click_log.simple_verbosity_option(logs.logger, expose_value=True) @click.pass_context -def cli(ctx, env_file, url, verbosity, token, quiet): +def cli(ctx, env_file, url, verbosity, token, quiet, no_login): + # small hack to fix some weird issues with pyinstaller and keyring + # there seems to be a cache issue somewhere + del keyring.backend.get_all_keyring.__wrapped__.always_returns + keyring.core.init_backend() + # /end of hack ctx.ensure_object(dict) logs.logger.disabled = quiet - set_server(ctx, url, token) + set_server(ctx, url, token, use_auth=not no_login) @cli.command() diff --git a/requirements-dev.txt b/requirements-dev.txt new file mode 100644 index 0000000000000000000000000000000000000000..cd76dde93067bea93e8d81d5fa70ac13b10a907a --- /dev/null +++ b/requirements-dev.txt @@ -0,0 +1,19 @@ +aiofiles +aiohttp +appdirs +click +click-log +keyring +marshmallow +python-dotenv +semver +tabulate +tqdm +pathvalidate +aioresponses +asynctest +ipdb +pytest +pytest-mock +pytest-env +pyinstaller diff --git a/setup.cfg b/setup.cfg index a32bac142ab34dc26ce0e5466b25d65a2c82b3be..37b52206a221db941a37498121cf842214dad928 100644 --- a/setup.cfg +++ b/setup.cfg @@ -44,7 +44,7 @@ dev = pytest pytest-mock pytest-env - + pyinstaller [options.packages.find]