Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a PyInstaller build for unsupported distros #27

Merged
merged 48 commits into from
Aug 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
48 commits
Select commit Hold shift + click to select a range
fdaba4b
Add PyInstaller
velovix May 22, 2021
890ceb8
Store the binary as a CI artifact
velovix May 22, 2021
6e0dfa8
Undo README changes
velovix May 22, 2021
98efea5
Update upload experience
velovix May 28, 2021
1f10b0a
Fix upload naming in CI
velovix May 28, 2021
a78dfb4
Install the package itself, too!
velovix May 28, 2021
80d61d2
Make upload path configurable
velovix May 28, 2021
e34b0d0
Fix upload_file call
velovix May 28, 2021
4ed3d98
Remove preceeding slash from S3 key
velovix May 28, 2021
25fbf3e
Update self-update
velovix May 28, 2021
7109665
Fix invalidation
velovix May 28, 2021
5c8f381
Fix invalidation quantity
velovix May 28, 2021
42c41ca
Add credentials to latest request
velovix May 28, 2021
284a327
Try bumping version
velovix May 28, 2021
a8a4118
Bump version again (oops)
velovix May 28, 2021
13844a3
Revert "Bump version again (oops)"
velovix May 28, 2021
132ca1a
Revert "Try bumping version"
velovix May 28, 2021
318c5ca
Fix the most basic of logic
velovix May 28, 2021
1741ffd
Try bumping version
velovix May 28, 2021
e9f540c
Revert "Try bumping version"
velovix May 28, 2021
fca9671
Formatting
velovix May 28, 2021
78394e5
Undo botched merge
velovix May 29, 2021
54e3a65
Add removed comment
velovix May 29, 2021
ff3e4c2
Undo setdefault regression
velovix May 29, 2021
f3a2f7b
Improve missing-defaults-file wording
velovix May 29, 2021
ab4b7eb
Improve version-failing message wording
velovix May 29, 2021
e6245bb
Improve print formatting
velovix May 29, 2021
008ebab
Remove version_tag
velovix May 29, 2021
924c490
Merge remote-tracking branch 'origin/feature/13-pyinstaller' into fea…
velovix May 29, 2021
637e67f
Remove unused docker-compose messages
velovix May 29, 2021
a014f83
Improve version-failing wording
velovix May 29, 2021
a0346f4
Formatting
velovix May 29, 2021
94b6d9c
Use existing fail function
velovix Jun 4, 2021
011e4b4
Remove unneeded import
velovix Jun 4, 2021
6bdbdaf
Remove getattr use
velovix Jul 23, 2021
38d959a
Download to a tempfile, copy when download done
velovix Jul 23, 2021
60897d5
Fix get_latest_version credentials handling
velovix Jul 23, 2021
545ef74
ResourceNotFoundError -> FileNotFoundError
velovix Jul 23, 2021
6f93264
Simplify resource loading logic
velovix Jul 23, 2021
93ee792
Add error when the translations file is missing
velovix Jul 23, 2021
e3f7f6f
Don't try to translate the translate error
velovix Jul 27, 2021
00f3eb1
Use pathlib permissions methods
velovix Jul 27, 2021
96ad3d9
Switch to resolving absolute paths at import-time
velovix Jul 28, 2021
1ad2fac
Fix PyInstaller build
velovix Jul 29, 2021
5f95a66
Check absolute resource paths at import-time
velovix Aug 2, 2021
478c1c4
Allow resources to be directories
velovix Aug 2, 2021
d069c2c
Formatting
velovix Aug 2, 2021
7ed5645
Add a Docker Compose check for frozen builds
velovix Aug 2, 2021
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
73 changes: 70 additions & 3 deletions .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,29 @@ workflows:
- test-installation-deb:
requires:
- build-deb
binary:
jobs:
- build-binary
- test-installation-binary:
requires:
- build-binary
- upload-binary:
name: upload-binary-staging
context: aws-staging
requires:
- test-installation-binary
- upload-binary:
name: upload-binary-production
context: aws
requires:
- upload-binary-staging
filters:
# Ignore any commit on any branch by default
branches:
ignore: /.*/
# Run only when a tag is created
tags:
only: /^v.+\..+\..+/
BryceBeagle marked this conversation as resolved.
Show resolved Hide resolved
deploy:
jobs:
- upload-to-pypi:
Expand Down Expand Up @@ -61,7 +84,7 @@ jobs:
executor: << parameters.os >>
steps:
- run: sudo apt-get update
- run: sudo apt-get install -y software-properties-common dpkg-dev devscripts equivs
- run: sudo apt-get install -y software-properties-common dpkg-dev devscripts equivs python3-pip
- run: sudo add-apt-repository ppa:jyrki-pulliainen/dh-virtualenv
- run: sudo apt-get install -y dh-virtualenv
- install-poetry
Expand Down Expand Up @@ -112,7 +135,7 @@ jobs:
image: ubuntu-2004:202010-01
steps:
- run: sudo apt-get update
- run: sudo apt-get install python3 python3-dev curl git -y
- run: sudo apt-get -y install python3-pip python3-dev curl git
- checkout
- install-poetry
- run: sudo $(which poetry) install
Expand All @@ -122,10 +145,54 @@ jobs:
- assert-core-responds-to-http
- run: sudo $(which poetry) run brainframe compose down

build-binary:
docker:
# CentOS 7 is the oldest Linux distribution we unofficially support.
# Building on here allows us to link to a very old version of libc,
# which will work on all more modern distributions.
- image: centos:7
environment:
PYTHONIOENCODING: utf8
steps:
- checkout
- run: yum -y install python3
- install-poetry
- run: poetry install --no-root
- run: PYTHONPATH=$PYTHONPATH:. poetry run pyinstaller package/main.spec
- store_artifacts:
path: dist
- persist_to_workspace:
root: .
paths:
- dist/*

upload-binary:
docker:
- image: cimg/python:3.6
steps:
- attach_workspace:
at: /tmp/workspace
- checkout
- run: poetry install
- run: poetry run python package/upload_binary.py --binary-path /tmp/workspace/dist/brainframe

test-installation-binary:
machine:
image: ubuntu-2004:202010-01
steps:
- run: sudo apt-get update
- attach_workspace:
at: /tmp/workspace
- run: sudo cp /tmp/workspace/dist/brainframe /usr/local/bin/brainframe
- run: sudo brainframe install --noninteractive
- run: sudo brainframe compose up -d
- assert-core-container-running
- assert-core-responds-to-http
- run: sudo brainframe compose down

commands:
install-poetry:
steps:
- run: sudo apt-get update && sudo apt-get install -y python3-pip
- run: >
curl -sSL https://raw.githubusercontent.com/python-poetry/poetry/master/get-poetry.py
| python3 - --version << pipeline.parameters.poetry-version >>
Expand Down
10 changes: 9 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
# Tooling
.mypy_cache/
dist/

# IDEs
.idea/

# Python
__pycache__
*.pyc
*.egg-info/

# PyInstaller
dist/
build/
1 change: 1 addition & 0 deletions brainframe/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
__version__ = "0.2.0"
1 change: 1 addition & 0 deletions brainframe/cli/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,6 @@
from .compose import compose
from .info import info
from .install import install
from .self_update import self_update
from .update import update
from .utils import by_name
19 changes: 10 additions & 9 deletions brainframe/cli/commands/install.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import shutil
from argparse import ArgumentParser
from pathlib import Path

Expand All @@ -6,6 +7,7 @@
config,
dependencies,
docker_compose,
frozen_utils,
os_utils,
print_utils,
)
Expand All @@ -30,10 +32,14 @@ def install():
print()

# Check all dependencies
dependencies.curl.ensure(args.noninteractive, args.install_curl)
dependencies.docker.ensure(args.noninteractive, args.install_docker)
# We only require the Docker Compose command in frozen distributions
if frozen_utils.is_frozen() and shutil.which("docker-compose") is None:
print_utils.fail_translate(
"install.install-dependency-manually", dependency="docker-compose",
)

_, _, download_version = docker_compose.check_download_version()
download_version = docker_compose.get_latest_version()
print_utils.translate("install.install-version", version=download_version)

if not os_utils.added_to_group("docker"):
Expand Down Expand Up @@ -123,8 +129,8 @@ def install():
print_utils.translate("install.set-custom-directory-env-vars")
print(
f"\n"
f'export {config.install_path.name}="{install_path}"\n'
f'export {config.data_path.name}="{data_path}"\n'
f'export {config.install_path.env_var_name}="{install_path}"\n'
f'export {config.data_path.env_var_name}="{data_path}"\n'
)


Expand Down Expand Up @@ -156,11 +162,6 @@ def _parse_args():
action="store_true",
help=i18n.t("install.install-docker-help"),
)
parser.add_argument(
"--install-curl",
action="store_true",
help=i18n.t("install.install-curl-help"),
)
parser.add_argument(
"--add-to-group",
action="store_true",
Expand Down
117 changes: 117 additions & 0 deletions brainframe/cli/commands/self_update.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import os
import shutil
import stat
import sys
from argparse import ArgumentParser
from pathlib import Path
from tempfile import NamedTemporaryFile
from typing import Optional, Tuple, Union

import i18n
import requests
from brainframe.cli import __version__, config, frozen_utils, print_utils
from packaging import version

from .utils import command

_RELEASES_URL_PREFIX = "https://{subdomain}aotu.ai"
_BINARY_URL = "{prefix}/releases/brainframe-cli/brainframe"
_LATEST_TAG_URL = "{prefix}/releases/brainframe-cli/latest"


@command("self-update")
def self_update():
_parse_args()

if not frozen_utils.is_frozen():
print_utils.fail_translate("self-update.not-frozen")

executable_path = Path(sys.executable)

# Check if the user has permissions to overwrite the executable in its
# current location
if not os.access(executable_path, os.W_OK):
error_message = i18n.t(
"general.file-bad-write-permissions", path=executable_path
)
error_message += "\n"
error_message += i18n.t("general.retry-as-root")
print_utils.fail(error_message)

credentials = config.staging_credentials()
apockill marked this conversation as resolved.
Show resolved Hide resolved

prefix = _RELEASES_URL_PREFIX.format(
subdomain="staging." if config.is_staging.value else ""
)
binary_url = _BINARY_URL.format(prefix=prefix)

current_version = version.parse(__version__)
latest_version = _latest_version(prefix, credentials)

if current_version >= latest_version:
print_utils.fail_translate(
"self-update.already-up-to-date",
current_version=current_version,
latest_version=latest_version,
)

# Get the updated executable
print_utils.translate("self-update.downloading")
response = requests.get(binary_url, auth=credentials, stream=True)
if not response.ok:
print_utils.fail_translate(
"self-update.error-downloading",
status_code=response.status_code,
error_message=response.text,
)

with NamedTemporaryFile("wb") as new_executable:
for block in response.iter_content(_BLOCK_SIZE):
new_executable.write(block)
new_executable.flush()

# Set the result as executable
current_stat = executable_path.stat()
executable_path.chmod(
current_stat.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH
)

# Overwrite the existing executable with the new one
# Really excited for copy3, coming this summer
shutil.copy2(new_executable.name, executable_path)

print()
print_utils.translate(
"self-update.complete", color=print_utils.Color.GREEN
)


def _latest_version(
url_prefix: str, credentials: Optional[Tuple[str, str]],
) -> Union[version.LegacyVersion, version.Version]:
latest_tag_url = _LATEST_TAG_URL.format(prefix=url_prefix)
response = requests.get(latest_tag_url, auth=credentials)

if not response.ok:
print_utils.fail_translate(
"self-update.error-getting-latest-version",
status_code=response.status_code,
error_message=response.text,
)

return version.parse(response.text)


def _parse_args():
parser = ArgumentParser(
description=i18n.t("self-update.description"),
usage=i18n.t("self-update.usage"),
)

return parser.parse_args(sys.argv[2:])


_BLOCK_SIZE = 1024000
"""The block size to read files at. Chosen from this answer:
https://stackoverflow.com/a/3673731
"""
4 changes: 3 additions & 1 deletion brainframe/cli/commands/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ def update():
docker_compose.assert_installed(install_path)

if args.version == "latest":
_, _, requested_version_str = docker_compose.check_download_version()
requested_version_str = docker_compose.check_existing_version(
install_path
)
else:
requested_version_str = args.version

Expand Down
33 changes: 21 additions & 12 deletions brainframe/cli/config.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,11 @@
import os
from distutils.util import strtobool
from pathlib import Path
from typing import Callable, Dict, Generic, Optional, TypeVar, Union
from typing import Callable, Dict, Generic, Optional, Tuple, TypeVar, Union

import yaml

from . import print_utils

_DEFAULTS_FILE_PATH = Path(__file__).parent / "defaults.yaml"

from . import frozen_utils, print_utils

T = TypeVar("T")

Expand Down Expand Up @@ -55,13 +52,7 @@ def load(

def load() -> None:
"""Initializes configuration options"""
if not _DEFAULTS_FILE_PATH.is_file():
print_utils.fail_translate(
"general.missing-defaults-file",
defaults_file_path=_DEFAULTS_FILE_PATH,
)

with _DEFAULTS_FILE_PATH.open("r") as defaults_file:
with frozen_utils.DEFAULTS_FILE_PATH.open("r") as defaults_file:
defaults = yaml.load(defaults_file, Loader=yaml.FullLoader)

install_path.load(Path, defaults)
Expand All @@ -72,6 +63,24 @@ def load() -> None:
staging_password.load(str, defaults)


def staging_credentials() -> Optional[Tuple[str, str]]:
if not is_staging.value:
return None

username = staging_username.value
password = staging_password.value
if username is None or password is None:
print_utils.fail_translate(
"general.staging-missing-credentials",
username_env_var=staging_username.env_var_name,
password_env_var=staging_password.env_var_name,
)

# Mypy doesn't understand that fail_translate exits this function, so it
# thinks the return type should be Tuple[Optional[str], Optional[str]]
return username, password # type: ignore


def _bool_converter(value: Union[str, bool]) -> bool:
if isinstance(value, bool):
return value
Expand Down
Loading