diff --git a/gel/__main__.py b/gel/__main__.py new file mode 100644 index 00000000..1b07b01e --- /dev/null +++ b/gel/__main__.py @@ -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() diff --git a/gel/cli.py b/gel/cli.py new file mode 100644 index 00000000..3d30ab8d --- /dev/null +++ b/gel/cli.py @@ -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() diff --git a/setup.py b/setup.py index 6c6684d6..aaccfcf0 100644 --- a/setup.py +++ b/setup.py @@ -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", ] } )