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 support for running (and installing) the main CLI #572

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
23 changes: 23 additions & 0 deletions gel/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
#
# This source file is part of the EdgeDB open source project.
#
# Copyright 2022-present MagicStack Inc. and the EdgeDB authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#


from .cli import main

if __name__ == "__main__":
main()
271 changes: 271 additions & 0 deletions gel/cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
#
# This source file is part of the EdgeDB open source project.
#
# Copyright 2016-present MagicStack Inc. and the EdgeDB authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#

from typing import (
NoReturn,
Tuple,
)

import os
import os.path
import pathlib
import platform
import shutil
import ssl
import stat
import subprocess
import sys
import tempfile
import time
import urllib.request


PACKAGE_URL_PREFIX = "https://packages.edgedb.com/dist"
STRONG_CIPHERSUITES = ":".join([
"TLS_AES_128_GCM_SHA256",
"TLS_CHACHA20_POLY1305_SHA256",
"TLS_AES_256_GCM_SHA384",
"ECDHE-ECDSA-AES128-GCM-SHA256",
"ECDHE-RSA-AES128-GCM-SHA256",
"ECDHE-ECDSA-CHACHA20-POLY1305",
"ECDHE-RSA-CHACHA20-POLY1305",
"ECDHE-ECDSA-AES256-GCM-SHA384",
"ECDHE-RSA-AES256-GCM-SHA384",
])


def _die(msg: str) -> NoReturn:
print(f"error: {msg}", file=sys.stderr)
sys.exit(1)


def _warn(msg: str) -> NoReturn:
print(f"warning: {msg}", file=sys.stderr)


def _run_cli(path: str) -> NoReturn:
cmd = [path] + sys.argv[1:]
if os.name == "nt":
result = subprocess.run(cmd)
sys.exit(result.returncode)
else:
os.execv(path, cmd)


def _real_mac_machine(machine: str) -> str:
import ctypes
import ctypes.util

def _sysctl(libc: ctypes.CDLL, name: str) -> str:
size = ctypes.c_uint(0)
libc.sysctlbyname(name, None, ctypes.byref(size), None, 0)
buf = ctypes.create_string_buffer(size.value)
libc.sysctlbyname(name, buf, ctypes.byref(size), None, 0)
return buf.value

libc_path = ctypes.util.find_library("c")
if not libc_path:
_die("could not find the C library")
libc = ctypes.CDLL(libc_path)
if machine == "i386":
# check for 32-bit emulation on a 64-bit x86 machine
if _sysctl(libc, "hw.optional.x86_64") == "1":
machine = "x86_64"
elif machine == "x86_64":
# check for Rosetta
if _sysctl(libc, "sysctl.proc_translated") == "1":
machine = "aarch64"

return machine


def _platform() -> Tuple[str, str]:
uname = platform.uname()
uname_sys = uname.system
machine = uname.machine.lower()
if (
uname_sys == "Windows"
or uname_sys.startswith("CYGWIN_NT")
or uname_sys.startswith("MINGW64_NT")
or uname_sys.startswith("MSYS_NT")
):
os = "Windows"
elif uname_sys == "Darwin":
if machine == "i386" or machine == "x86_64":
machine = _real_mac_machine(machine)
os = "Darwin"
elif uname_sys == "Linux":
os = "Linux"
else:
_die(f"unsupported OS: {uname_sys}")

if machine in ("x86-64", "x64", "amd64"):
machine = "x86_64"
elif machine == "arm64":
machine = "aarch64"

if machine not in ("x86_64", "aarch64") or (
machine == "aarch64" and os not in ("Darwin", "Linux")
):
_die(f"unsupported hardware architecture: {machine}")

return os, machine


def _download(url: str, dest: pathlib.Path) -> None:
if not url.lower().startswith("https://"):
_die(f"unexpected insecure URL: {url}")

# Create an SSL context with certificate verification enabled
ssl_context = ssl.create_default_context()
ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2
ssl_context.set_ciphers(STRONG_CIPHERSUITES)

try:
# Open the URL with the SSL context
with urllib.request.urlopen(url, context=ssl_context) as response:
final_url = response.geturl()
if not final_url.lower().startswith("https://"):
_die("redirected to a non-HTTPS URL, download aborted.")

if response.status != 200:
raise RuntimeError(f"{response.status}")

spinner_symbols = ['|', '/', '-', '\\']
msg = "downloading Gel CLI"
print(f"{msg}", end="\r")
start = time.monotonic()

with open(str(dest), mode="wb") as file:
i = 0
while True:
chunk = response.read(524288)
if not chunk:
break
file.write(chunk)
now = time.monotonic()
if now - start > 0.2:
print(f"\r{msg} {spinner_symbols[i]}", end="\r")
start = now
i = (i + 1) % len(spinner_symbols)

# clear
print(f"{' ' * (len(msg) + 2)}", end="\r")

except Exception as e:
_die(f"could not download Gel CLI: {e}")


def _get_binary_cache_dir(os_name) -> pathlib.Path:
home = pathlib.Path.home()
if os_name == 'Windows':
localappdata = os.environ.get('LOCALAPPDATA', '')
if localappdata:
base_cache_dir = pathlib.Path(localappdata)
else:
base_cache_dir = home / 'AppData' / 'Local'
elif os_name == 'Linux':
xdg_cache_home = os.environ.get('XDG_CACHE_HOME', '')
if xdg_cache_home:
base_cache_dir = pathlib.Path(xdg_cache_home)
else:
base_cache_dir = home / '.cache'
elif os_name == 'Darwin':
base_cache_dir = home / 'Library' / 'Caches'
else:
_die(f"unsupported OS: {os_name}")

cache_dir = base_cache_dir / "gel" / "bin"
try:
cache_dir.mkdir(parents=True, exist_ok=True)
except OSError as e:
_warn(f"could not create {cache_dir}: {e}")

try:
cache_dir = pathlib.Path(tempfile.mkdtemp(prefix="gel"))
except Exception as e:
_die(f"could not create temporary directory: {e}")

return cache_dir


def _get_mountpoint(path: pathlib.Path) -> pathlib.Path:
path = path.resolve()
if os.path.ismount(str(path)):
return path
else:
for p in path.parents:
if os.path.ismount(str(p)):
return p

return p


def _install_cli(os_name: str, arch: str, path: pathlib.Path) -> str:
triple = f"{arch}"
ext = ""
if os_name == "Windows":
triple += "-pc-windows-msvc"
ext = ".exe"
elif os_name == "Darwin":
triple += "-apple-darwin"
elif os_name == "Linux":
triple += "-unknown-linux-musl"
else:
_die(f"unexpected OS: {os}")

url = f"{PACKAGE_URL_PREFIX}/{triple}/edgedb-cli{ext}"

if path.exists() and not path.is_file():
_die(f"{path} exists but is not a regular file, "
f"please remove it and try again")

_download(url, path)

try:
path.chmod(
stat.S_IRWXU
| stat.S_IRGRP | stat.S_IXGRP
| stat.S_IROTH | stat.S_IXOTH,
)
except OSError as e:
_die(f"could not max {path!r} executable: {e}")

if not os.access(str(path), os.X_OK):
_die(
f"cannot execute {path!r} "
f"(likely because {_get_mountpoint(path)} is mounted as noexec)"
)


def main() -> NoReturn:
dev_cli = shutil.which("gel-dev")
if dev_cli:
path = pathlib.Path(dev_cli)
else:
os, arch = _platform()
cache_dir = _get_binary_cache_dir(os)
path = cache_dir / "gel"
if not path.exists():
_install_cli(os, arch, path)

_run_cli(path)


if __name__ == "__main__":
main()
1 change: 1 addition & 0 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -366,6 +366,7 @@ def finalize_options(self):
"edgedb-py=gel.codegen.cli:main",
"gel-py=gel.codegen.cli:main",
"gel-orm=gel.orm.cli:main",
"gel=gel.cli:main",
]
}
)
Loading