mirror of
https://github.com/astral-sh/uv.git
synced 2025-07-07 13:25:00 +00:00
Copy over gourgeist
crate (#61)
This PR copies over the `gourgeist` crate at commit `e64c17a263dac6933702dc8d155425c053fe885a` with no modifications. It won't pass CI, but modifications will intentionally be confined to later PRs.
This commit is contained in:
parent
d1ed41170b
commit
7caf5f42b8
24 changed files with 2980 additions and 0 deletions
1450
crates/gourgeist/Cargo.lock
generated
Normal file
1450
crates/gourgeist/Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
61
crates/gourgeist/Cargo.toml
Normal file
61
crates/gourgeist/Cargo.toml
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
[package]
|
||||||
|
name = "gourgeist"
|
||||||
|
version = "0.0.4"
|
||||||
|
edition = "2021"
|
||||||
|
description = "virtualenv creation implemented in rust"
|
||||||
|
repository = "https://github.com/konstin/gourgeist"
|
||||||
|
license = "MIT OR Apache-2.0"
|
||||||
|
keywords = ["virtualenv", "venv", "python"]
|
||||||
|
readme = "Readme.md"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
camino = { version = "1.1.6", features = ["serde1"] }
|
||||||
|
clap = { version = "4.4.5", features = ["derive"] }
|
||||||
|
configparser = "3.0.2"
|
||||||
|
dirs = "5.0.1"
|
||||||
|
fs-err = "2.9.0"
|
||||||
|
install-wheel-rs = { version = "0.0.1", optional = true }
|
||||||
|
minreq = { version = "2.10.0", optional = true, features = ["https"] }
|
||||||
|
rayon = { version = "1.8.0", optional = true }
|
||||||
|
seahash = "4.1.0"
|
||||||
|
serde = { version = "1.0.188", features = ["derive"] }
|
||||||
|
serde_json = "1.0.107"
|
||||||
|
tempfile = "3.8.0"
|
||||||
|
thiserror = "1.0.49"
|
||||||
|
tracing = "0.1.37"
|
||||||
|
tracing-subscriber = { version = "0.3.17", features = ["env-filter"] }
|
||||||
|
which = "4.4.2"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["install"]
|
||||||
|
install = ["install-wheel-rs", "minreq"]
|
||||||
|
parallel = ["rayon"]
|
||||||
|
|
||||||
|
# zip implementation
|
||||||
|
[profile.dev.package.adler]
|
||||||
|
opt-level = 3
|
||||||
|
|
||||||
|
[profile.profiling]
|
||||||
|
inherits = "release"
|
||||||
|
lto = "thin"
|
||||||
|
debug = true
|
||||||
|
|
||||||
|
# The profile that 'cargo dist' will build with
|
||||||
|
[profile.dist]
|
||||||
|
inherits = "release"
|
||||||
|
lto = "thin"
|
||||||
|
|
||||||
|
# Config for 'cargo dist'
|
||||||
|
[workspace.metadata.dist]
|
||||||
|
# The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax)
|
||||||
|
cargo-dist-version = "0.3.1"
|
||||||
|
# CI backends to support
|
||||||
|
ci = ["github"]
|
||||||
|
# The installers to generate for each app
|
||||||
|
installers = ["shell", "powershell"]
|
||||||
|
# Target platforms to build apps for (Rust target-triple syntax)
|
||||||
|
targets = ["x86_64-unknown-linux-gnu", "aarch64-apple-darwin", "x86_64-apple-darwin", "x86_64-pc-windows-msvc"]
|
||||||
|
# Publish jobs to run in CI
|
||||||
|
pr-run-mode = "plan"
|
33
crates/gourgeist/Readme.md
Normal file
33
crates/gourgeist/Readme.md
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
# Gourgeist
|
||||||
|
|
||||||
|
Gourgeist is a rust library to create python virtual environments. It also has a CLI.
|
||||||
|
|
||||||
|
It currently supports only unix (linux/mac), windows support is missing.
|
||||||
|
|
||||||
|
## Rust
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use camino::Utf8PathBuf;
|
||||||
|
use gourgeist::{create_venv, get_interpreter_info, parse_python_cli};
|
||||||
|
|
||||||
|
let location = cli.path.unwrap_or(Utf8PathBuf::from(".venv"));
|
||||||
|
let python = parse_python_cli(cli.python)?;
|
||||||
|
let data = get_interpreter_info(&python)?;
|
||||||
|
create_venv(&location, &python, &data, cli.bare)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
## CLI
|
||||||
|
|
||||||
|
Use `python` as base for a virtualenv `.venv`:
|
||||||
|
```bash
|
||||||
|
gourgeist
|
||||||
|
```
|
||||||
|
|
||||||
|
Or use custom defaults:
|
||||||
|
```bash
|
||||||
|
gourgeist -p 3.11 my_env
|
||||||
|
```
|
||||||
|
|
||||||
|
## Jessie's gourgeist
|
||||||
|
|
||||||
|

|
16
crates/gourgeist/benchmark.sh
Normal file
16
crates/gourgeist/benchmark.sh
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
virtualenv --version
|
||||||
|
|
||||||
|
#cargo build --profile profiling
|
||||||
|
cargo build --release #--features parallel
|
||||||
|
# Benchmarking trick! strip your binaries ٩( ∂‿∂ )۶
|
||||||
|
strip target/release/gourgeist
|
||||||
|
|
||||||
|
echo "## Bare"
|
||||||
|
hyperfine --warmup 1 --prepare "rm -rf target/a" "virtualenv -p 3.11 --no-seed target/a" "target/release/gourgeist -p 3.11 --bare target/a"
|
||||||
|
echo "## Default"
|
||||||
|
hyperfine --warmup 1 --prepare "rm -rf target/a" "virtualenv -p 3.11 target/a" "target/release/gourgeist -p 3.11 target/a"
|
||||||
|
|
27
crates/gourgeist/compare_in_git.sh
Normal file
27
crates/gourgeist/compare_in_git.sh
Normal file
|
@ -0,0 +1,27 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
virtualenv_command() {
|
||||||
|
virtualenv -p 3.11 compare_venv # --no-pip --no-setuptools --no-wheel
|
||||||
|
}
|
||||||
|
rust_command() {
|
||||||
|
cargo run -- -p 3.11 compare_venv # --bare
|
||||||
|
}
|
||||||
|
|
||||||
|
rm -rf compare_venv
|
||||||
|
virtualenv_command
|
||||||
|
rm compare_venv/.gitignore
|
||||||
|
git -C compare_venv init
|
||||||
|
git -C compare_venv add -A
|
||||||
|
git -C compare_venv commit -q -m "Initial commit"
|
||||||
|
rm -r compare_venv/* # This skips the hidden .git
|
||||||
|
mkdir -p target
|
||||||
|
mv compare_venv target/compare_venv2
|
||||||
|
rust_command
|
||||||
|
rm compare_venv/.gitignore
|
||||||
|
cp -r compare_venv/* target/compare_venv2
|
||||||
|
rm -r compare_venv
|
||||||
|
mv target/compare_venv2 compare_venv
|
||||||
|
git -C compare_venv/ status
|
||||||
|
|
12
crates/gourgeist/imasnake.py
Normal file
12
crates/gourgeist/imasnake.py
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
print(sys.executable)
|
||||||
|
print(sys.version)
|
||||||
|
print(sys.base_prefix)
|
||||||
|
print(sys.prefix)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
8
crates/gourgeist/oranda.json
Normal file
8
crates/gourgeist/oranda.json
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"styles": {
|
||||||
|
"theme": "axo_light"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"path_prefix": "gourgeist"
|
||||||
|
}
|
||||||
|
}
|
102
crates/gourgeist/src/_virtualenv.py
Normal file
102
crates/gourgeist/src/_virtualenv.py
Normal file
|
@ -0,0 +1,102 @@
|
||||||
|
"""Patches that are applied at runtime to the virtual environment."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from contextlib import suppress
|
||||||
|
|
||||||
|
VIRTUALENV_PATCH_FILE = os.path.join(__file__)
|
||||||
|
|
||||||
|
|
||||||
|
def patch_dist(dist):
|
||||||
|
"""
|
||||||
|
Distutils allows user to configure some arguments via a configuration file:
|
||||||
|
https://docs.python.org/3/install/index.html#distutils-configuration-files.
|
||||||
|
|
||||||
|
Some of this arguments though don't make sense in context of the virtual environment files, let's fix them up.
|
||||||
|
""" # noqa: D205
|
||||||
|
# we cannot allow some install config as that would get packages installed outside of the virtual environment
|
||||||
|
old_parse_config_files = dist.Distribution.parse_config_files
|
||||||
|
|
||||||
|
def parse_config_files(self, *args, **kwargs):
|
||||||
|
result = old_parse_config_files(self, *args, **kwargs)
|
||||||
|
install = self.get_option_dict("install")
|
||||||
|
|
||||||
|
if "prefix" in install: # the prefix governs where to install the libraries
|
||||||
|
install["prefix"] = VIRTUALENV_PATCH_FILE, os.path.abspath(sys.prefix)
|
||||||
|
for base in ("purelib", "platlib", "headers", "scripts", "data"):
|
||||||
|
key = f"install_{base}"
|
||||||
|
if key in install: # do not allow global configs to hijack venv paths
|
||||||
|
install.pop(key, None)
|
||||||
|
return result
|
||||||
|
|
||||||
|
dist.Distribution.parse_config_files = parse_config_files
|
||||||
|
|
||||||
|
|
||||||
|
# Import hook that patches some modules to ignore configuration values that break package installation in case
|
||||||
|
# of virtual environments.
|
||||||
|
_DISTUTILS_PATCH = "distutils.dist", "setuptools.dist"
|
||||||
|
# https://docs.python.org/3/library/importlib.html#setting-up-an-importer
|
||||||
|
|
||||||
|
|
||||||
|
class _Finder:
|
||||||
|
"""A meta path finder that allows patching the imported distutils modules."""
|
||||||
|
|
||||||
|
fullname = None
|
||||||
|
|
||||||
|
# lock[0] is threading.Lock(), but initialized lazily to avoid importing threading very early at startup,
|
||||||
|
# because there are gevent-based applications that need to be first to import threading by themselves.
|
||||||
|
# See https://github.com/pypa/virtualenv/issues/1895 for details.
|
||||||
|
lock = [] # noqa: RUF012
|
||||||
|
|
||||||
|
def find_spec(self, fullname, path, target=None): # noqa: ARG002
|
||||||
|
if fullname in _DISTUTILS_PATCH and self.fullname is None:
|
||||||
|
# initialize lock[0] lazily
|
||||||
|
if len(self.lock) == 0:
|
||||||
|
import threading
|
||||||
|
|
||||||
|
lock = threading.Lock()
|
||||||
|
# there is possibility that two threads T1 and T2 are simultaneously running into find_spec,
|
||||||
|
# observing .lock as empty, and further going into hereby initialization. However due to the GIL,
|
||||||
|
# list.append() operation is atomic and this way only one of the threads will "win" to put the lock
|
||||||
|
# - that every thread will use - into .lock[0].
|
||||||
|
# https://docs.python.org/3/faq/library.html#what-kinds-of-global-value-mutation-are-thread-safe
|
||||||
|
self.lock.append(lock)
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
|
from importlib.util import find_spec
|
||||||
|
|
||||||
|
with self.lock[0]:
|
||||||
|
self.fullname = fullname
|
||||||
|
try:
|
||||||
|
spec = find_spec(fullname, path)
|
||||||
|
if spec is not None:
|
||||||
|
# https://www.python.org/dev/peps/pep-0451/#how-loading-will-work
|
||||||
|
is_new_api = hasattr(spec.loader, "exec_module")
|
||||||
|
func_name = "exec_module" if is_new_api else "load_module"
|
||||||
|
old = getattr(spec.loader, func_name)
|
||||||
|
func = self.exec_module if is_new_api else self.load_module
|
||||||
|
if old is not func:
|
||||||
|
with suppress(AttributeError): # C-Extension loaders are r/o such as zipimporter with <3.7
|
||||||
|
setattr(spec.loader, func_name, partial(func, old))
|
||||||
|
return spec
|
||||||
|
finally:
|
||||||
|
self.fullname = None
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def exec_module(old, module):
|
||||||
|
old(module)
|
||||||
|
if module.__name__ in _DISTUTILS_PATCH:
|
||||||
|
patch_dist(module)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def load_module(old, name):
|
||||||
|
module = old(name)
|
||||||
|
if module.__name__ in _DISTUTILS_PATCH:
|
||||||
|
patch_dist(module)
|
||||||
|
return module
|
||||||
|
|
||||||
|
|
||||||
|
sys.meta_path.insert(0, _Finder())
|
87
crates/gourgeist/src/activate
Normal file
87
crates/gourgeist/src/activate
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
# This file must be used with "source bin/activate" *from bash*
|
||||||
|
# you cannot run it directly
|
||||||
|
|
||||||
|
|
||||||
|
if [ "${BASH_SOURCE-}" = "$0" ]; then
|
||||||
|
echo "You must source this script: \$ source $0" >&2
|
||||||
|
exit 33
|
||||||
|
fi
|
||||||
|
|
||||||
|
deactivate () {
|
||||||
|
unset -f pydoc >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# reset old environment variables
|
||||||
|
# ! [ -z ${VAR+_} ] returns true if VAR is declared at all
|
||||||
|
if ! [ -z "${_OLD_VIRTUAL_PATH:+_}" ] ; then
|
||||||
|
PATH="$_OLD_VIRTUAL_PATH"
|
||||||
|
export PATH
|
||||||
|
unset _OLD_VIRTUAL_PATH
|
||||||
|
fi
|
||||||
|
if ! [ -z "${_OLD_VIRTUAL_PYTHONHOME+_}" ] ; then
|
||||||
|
PYTHONHOME="$_OLD_VIRTUAL_PYTHONHOME"
|
||||||
|
export PYTHONHOME
|
||||||
|
unset _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The hash command must be called to get it to forget past
|
||||||
|
# commands. Without forgetting past commands the $PATH changes
|
||||||
|
# we made may not be respected
|
||||||
|
hash -r 2>/dev/null
|
||||||
|
|
||||||
|
if ! [ -z "${_OLD_VIRTUAL_PS1+_}" ] ; then
|
||||||
|
PS1="$_OLD_VIRTUAL_PS1"
|
||||||
|
export PS1
|
||||||
|
unset _OLD_VIRTUAL_PS1
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset VIRTUAL_ENV
|
||||||
|
unset VIRTUAL_ENV_PROMPT
|
||||||
|
if [ ! "${1-}" = "nondestructive" ] ; then
|
||||||
|
# Self destruct!
|
||||||
|
unset -f deactivate
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# unset irrelevant variables
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
VIRTUAL_ENV='{{ VIRTUAL_ENV_TEMPLATE_STRING }}'
|
||||||
|
if ([ "$OSTYPE" = "cygwin" ] || [ "$OSTYPE" = "msys" ]) && $(command -v cygpath &> /dev/null) ; then
|
||||||
|
VIRTUAL_ENV=$(cygpath -u "$VIRTUAL_ENV")
|
||||||
|
fi
|
||||||
|
export VIRTUAL_ENV
|
||||||
|
|
||||||
|
_OLD_VIRTUAL_PATH="$PATH"
|
||||||
|
PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
export PATH
|
||||||
|
|
||||||
|
if [ "x" != x ] ; then
|
||||||
|
VIRTUAL_ENV_PROMPT=""
|
||||||
|
else
|
||||||
|
VIRTUAL_ENV_PROMPT=$(basename "$VIRTUAL_ENV")
|
||||||
|
fi
|
||||||
|
export VIRTUAL_ENV_PROMPT
|
||||||
|
|
||||||
|
# unset PYTHONHOME if set
|
||||||
|
if ! [ -z "${PYTHONHOME+_}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PYTHONHOME="$PYTHONHOME"
|
||||||
|
unset PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PS1="${PS1-}"
|
||||||
|
PS1="(${VIRTUAL_ENV_PROMPT}) ${PS1-}"
|
||||||
|
export PS1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Make sure to unalias pydoc if it's already there
|
||||||
|
alias pydoc 2>/dev/null >/dev/null && unalias pydoc || true
|
||||||
|
|
||||||
|
pydoc () {
|
||||||
|
python -m pydoc "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The hash command must be called to get it to forget past
|
||||||
|
# commands. Without forgetting past commands the $PATH changes
|
||||||
|
# we made may not be respected
|
||||||
|
hash -r 2>/dev/null
|
87
crates/gourgeist/src/activator/activate
Normal file
87
crates/gourgeist/src/activator/activate
Normal file
|
@ -0,0 +1,87 @@
|
||||||
|
# This file must be used with "source bin/activate" *from bash*
|
||||||
|
# you cannot run it directly
|
||||||
|
|
||||||
|
|
||||||
|
if [ "${BASH_SOURCE-}" = "$0" ]; then
|
||||||
|
echo "You must source this script: \$ source $0" >&2
|
||||||
|
exit 33
|
||||||
|
fi
|
||||||
|
|
||||||
|
deactivate () {
|
||||||
|
unset -f pydoc >/dev/null 2>&1 || true
|
||||||
|
|
||||||
|
# reset old environment variables
|
||||||
|
# ! [ -z ${VAR+_} ] returns true if VAR is declared at all
|
||||||
|
if ! [ -z "${_OLD_VIRTUAL_PATH:+_}" ] ; then
|
||||||
|
PATH="$_OLD_VIRTUAL_PATH"
|
||||||
|
export PATH
|
||||||
|
unset _OLD_VIRTUAL_PATH
|
||||||
|
fi
|
||||||
|
if ! [ -z "${_OLD_VIRTUAL_PYTHONHOME+_}" ] ; then
|
||||||
|
PYTHONHOME="$_OLD_VIRTUAL_PYTHONHOME"
|
||||||
|
export PYTHONHOME
|
||||||
|
unset _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The hash command must be called to get it to forget past
|
||||||
|
# commands. Without forgetting past commands the $PATH changes
|
||||||
|
# we made may not be respected
|
||||||
|
hash -r 2>/dev/null
|
||||||
|
|
||||||
|
if ! [ -z "${_OLD_VIRTUAL_PS1+_}" ] ; then
|
||||||
|
PS1="$_OLD_VIRTUAL_PS1"
|
||||||
|
export PS1
|
||||||
|
unset _OLD_VIRTUAL_PS1
|
||||||
|
fi
|
||||||
|
|
||||||
|
unset VIRTUAL_ENV
|
||||||
|
unset VIRTUAL_ENV_PROMPT
|
||||||
|
if [ ! "${1-}" = "nondestructive" ] ; then
|
||||||
|
# Self destruct!
|
||||||
|
unset -f deactivate
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# unset irrelevant variables
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
VIRTUAL_ENV='{{ VIRTUAL_ENV_DIR }}'
|
||||||
|
if ([ "$OSTYPE" = "cygwin" ] || [ "$OSTYPE" = "msys" ]) && $(command -v cygpath &> /dev/null) ; then
|
||||||
|
VIRTUAL_ENV=$(cygpath -u "$VIRTUAL_ENV")
|
||||||
|
fi
|
||||||
|
export VIRTUAL_ENV
|
||||||
|
|
||||||
|
_OLD_VIRTUAL_PATH="$PATH"
|
||||||
|
PATH="$VIRTUAL_ENV/bin:$PATH"
|
||||||
|
export PATH
|
||||||
|
|
||||||
|
if [ "x" != x ] ; then
|
||||||
|
VIRTUAL_ENV_PROMPT=""
|
||||||
|
else
|
||||||
|
VIRTUAL_ENV_PROMPT=$(basename "$VIRTUAL_ENV")
|
||||||
|
fi
|
||||||
|
export VIRTUAL_ENV_PROMPT
|
||||||
|
|
||||||
|
# unset PYTHONHOME if set
|
||||||
|
if ! [ -z "${PYTHONHOME+_}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PYTHONHOME="$PYTHONHOME"
|
||||||
|
unset PYTHONHOME
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT-}" ] ; then
|
||||||
|
_OLD_VIRTUAL_PS1="${PS1-}"
|
||||||
|
PS1="(${VIRTUAL_ENV_PROMPT}) ${PS1-}"
|
||||||
|
export PS1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Make sure to unalias pydoc if it's already there
|
||||||
|
alias pydoc 2>/dev/null >/dev/null && unalias pydoc || true
|
||||||
|
|
||||||
|
pydoc () {
|
||||||
|
python -m pydoc "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The hash command must be called to get it to forget past
|
||||||
|
# commands. Without forgetting past commands the $PATH changes
|
||||||
|
# we made may not be respected
|
||||||
|
hash -r 2>/dev/null
|
55
crates/gourgeist/src/activator/activate.csh
Normal file
55
crates/gourgeist/src/activator/activate.csh
Normal file
|
@ -0,0 +1,55 @@
|
||||||
|
# This file must be used with "source bin/activate.csh" *from csh*.
|
||||||
|
# You cannot run it directly.
|
||||||
|
# Created by Davide Di Blasi <davidedb@gmail.com>.
|
||||||
|
|
||||||
|
set newline='\
|
||||||
|
'
|
||||||
|
|
||||||
|
alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH:q" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT:q" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate && unalias pydoc'
|
||||||
|
|
||||||
|
# Unset irrelevant variables.
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
setenv VIRTUAL_ENV '{{ VIRTUAL_ENV_DIR }}'
|
||||||
|
|
||||||
|
set _OLD_VIRTUAL_PATH="$PATH:q"
|
||||||
|
setenv PATH "$VIRTUAL_ENV:q/bin:$PATH:q"
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if ('' != "") then
|
||||||
|
setenv VIRTUAL_ENV_PROMPT ''
|
||||||
|
else
|
||||||
|
setenv VIRTUAL_ENV_PROMPT "$VIRTUAL_ENV:t:q"
|
||||||
|
endif
|
||||||
|
|
||||||
|
if ( $?VIRTUAL_ENV_DISABLE_PROMPT ) then
|
||||||
|
if ( $VIRTUAL_ENV_DISABLE_PROMPT == "" ) then
|
||||||
|
set do_prompt = "1"
|
||||||
|
else
|
||||||
|
set do_prompt = "0"
|
||||||
|
endif
|
||||||
|
else
|
||||||
|
set do_prompt = "1"
|
||||||
|
endif
|
||||||
|
|
||||||
|
if ( $do_prompt == "1" ) then
|
||||||
|
# Could be in a non-interactive environment,
|
||||||
|
# in which case, $prompt is undefined and we wouldn't
|
||||||
|
# care about the prompt anyway.
|
||||||
|
if ( $?prompt ) then
|
||||||
|
set _OLD_VIRTUAL_PROMPT="$prompt:q"
|
||||||
|
if ( "$prompt:q" =~ *"$newline:q"* ) then
|
||||||
|
:
|
||||||
|
else
|
||||||
|
set prompt = '('"$VIRTUAL_ENV_PROMPT:q"') '"$prompt:q"
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
endif
|
||||||
|
|
||||||
|
unset env_name
|
||||||
|
unset do_prompt
|
||||||
|
|
||||||
|
alias pydoc python -m pydoc
|
||||||
|
|
||||||
|
rehash
|
103
crates/gourgeist/src/activator/activate.fish
Normal file
103
crates/gourgeist/src/activator/activate.fish
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
# This file must be used using `source bin/activate.fish` *within a running fish ( http://fishshell.com ) session*.
|
||||||
|
# Do not run it directly.
|
||||||
|
|
||||||
|
function _bashify_path -d "Converts a fish path to something bash can recognize"
|
||||||
|
set fishy_path $argv
|
||||||
|
set bashy_path $fishy_path[1]
|
||||||
|
for path_part in $fishy_path[2..-1]
|
||||||
|
set bashy_path "$bashy_path:$path_part"
|
||||||
|
end
|
||||||
|
echo $bashy_path
|
||||||
|
end
|
||||||
|
|
||||||
|
function _fishify_path -d "Converts a bash path to something fish can recognize"
|
||||||
|
echo $argv | tr ':' '\n'
|
||||||
|
end
|
||||||
|
|
||||||
|
function deactivate -d 'Exit virtualenv mode and return to the normal environment.'
|
||||||
|
# reset old environment variables
|
||||||
|
if test -n "$_OLD_VIRTUAL_PATH"
|
||||||
|
# https://github.com/fish-shell/fish-shell/issues/436 altered PATH handling
|
||||||
|
if test (echo $FISH_VERSION | head -c 1) -lt 3
|
||||||
|
set -gx PATH (_fishify_path "$_OLD_VIRTUAL_PATH")
|
||||||
|
else
|
||||||
|
set -gx PATH $_OLD_VIRTUAL_PATH
|
||||||
|
end
|
||||||
|
set -e _OLD_VIRTUAL_PATH
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -n "$_OLD_VIRTUAL_PYTHONHOME"
|
||||||
|
set -gx PYTHONHOME "$_OLD_VIRTUAL_PYTHONHOME"
|
||||||
|
set -e _OLD_VIRTUAL_PYTHONHOME
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -n "$_OLD_FISH_PROMPT_OVERRIDE"
|
||||||
|
and functions -q _old_fish_prompt
|
||||||
|
# Set an empty local `$fish_function_path` to allow the removal of `fish_prompt` using `functions -e`.
|
||||||
|
set -l fish_function_path
|
||||||
|
|
||||||
|
# Erase virtualenv's `fish_prompt` and restore the original.
|
||||||
|
functions -e fish_prompt
|
||||||
|
functions -c _old_fish_prompt fish_prompt
|
||||||
|
functions -e _old_fish_prompt
|
||||||
|
set -e _OLD_FISH_PROMPT_OVERRIDE
|
||||||
|
end
|
||||||
|
|
||||||
|
set -e VIRTUAL_ENV
|
||||||
|
set -e VIRTUAL_ENV_PROMPT
|
||||||
|
|
||||||
|
if test "$argv[1]" != 'nondestructive'
|
||||||
|
# Self-destruct!
|
||||||
|
functions -e pydoc
|
||||||
|
functions -e deactivate
|
||||||
|
functions -e _bashify_path
|
||||||
|
functions -e _fishify_path
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unset irrelevant variables.
|
||||||
|
deactivate nondestructive
|
||||||
|
|
||||||
|
set -gx VIRTUAL_ENV '{{ VIRTUAL_ENV_DIR }}'
|
||||||
|
|
||||||
|
# https://github.com/fish-shell/fish-shell/issues/436 altered PATH handling
|
||||||
|
if test (echo $FISH_VERSION | head -c 1) -lt 3
|
||||||
|
set -gx _OLD_VIRTUAL_PATH (_bashify_path $PATH)
|
||||||
|
else
|
||||||
|
set -gx _OLD_VIRTUAL_PATH $PATH
|
||||||
|
end
|
||||||
|
set -gx PATH "$VIRTUAL_ENV"'/bin' $PATH
|
||||||
|
|
||||||
|
# Prompt override provided?
|
||||||
|
# If not, just use the environment name.
|
||||||
|
if test -n ''
|
||||||
|
set -gx VIRTUAL_ENV_PROMPT ''
|
||||||
|
else
|
||||||
|
set -gx VIRTUAL_ENV_PROMPT (basename "$VIRTUAL_ENV")
|
||||||
|
end
|
||||||
|
|
||||||
|
# Unset `$PYTHONHOME` if set.
|
||||||
|
if set -q PYTHONHOME
|
||||||
|
set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME
|
||||||
|
set -e PYTHONHOME
|
||||||
|
end
|
||||||
|
|
||||||
|
function pydoc
|
||||||
|
python -m pydoc $argv
|
||||||
|
end
|
||||||
|
|
||||||
|
if test -z "$VIRTUAL_ENV_DISABLE_PROMPT"
|
||||||
|
# Copy the current `fish_prompt` function as `_old_fish_prompt`.
|
||||||
|
functions -c fish_prompt _old_fish_prompt
|
||||||
|
|
||||||
|
function fish_prompt
|
||||||
|
# Run the user's prompt first; it might depend on (pipe)status.
|
||||||
|
set -l prompt (_old_fish_prompt)
|
||||||
|
|
||||||
|
printf '(%s) ' $VIRTUAL_ENV_PROMPT
|
||||||
|
|
||||||
|
string join -- \n $prompt # handle multi-line prompts
|
||||||
|
end
|
||||||
|
|
||||||
|
set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV"
|
||||||
|
end
|
96
crates/gourgeist/src/activator/activate.nu
Normal file
96
crates/gourgeist/src/activator/activate.nu
Normal file
|
@ -0,0 +1,96 @@
|
||||||
|
# virtualenv activation module
|
||||||
|
# Activate with `overlay use activate.nu`
|
||||||
|
# Deactivate with `deactivate`, as usual
|
||||||
|
#
|
||||||
|
# To customize the overlay name, you can call `overlay use activate.nu as foo`,
|
||||||
|
# but then simply `deactivate` won't work because it is just an alias to hide
|
||||||
|
# the "activate" overlay. You'd need to call `overlay hide foo` manually.
|
||||||
|
|
||||||
|
export-env {
|
||||||
|
def is-string [x] {
|
||||||
|
($x | describe) == 'string'
|
||||||
|
}
|
||||||
|
|
||||||
|
def has-env [...names] {
|
||||||
|
$names | each {|n|
|
||||||
|
$n in $env
|
||||||
|
} | all {|i| $i == true}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Emulates a `test -z`, but btter as it handles e.g 'false'
|
||||||
|
def is-env-true [name: string] {
|
||||||
|
if (has-env $name) {
|
||||||
|
# Try to parse 'true', '0', '1', and fail if not convertible
|
||||||
|
let parsed = (do -i { $env | get $name | into bool })
|
||||||
|
if ($parsed | describe) == 'bool' {
|
||||||
|
$parsed
|
||||||
|
} else {
|
||||||
|
not ($env | get -i $name | is-empty)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let virtual_env = '{{ VIRTUAL_ENV_DIR }}'
|
||||||
|
let bin = 'bin'
|
||||||
|
|
||||||
|
let is_windows = ($nu.os-info.family) == 'windows'
|
||||||
|
let path_name = (if (has-env 'Path') {
|
||||||
|
'Path'
|
||||||
|
} else {
|
||||||
|
'PATH'
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
let venv_path = ([$virtual_env $bin] | path join)
|
||||||
|
let new_path = ($env | get $path_name | prepend $venv_path)
|
||||||
|
|
||||||
|
# If there is no default prompt, then use the env name instead
|
||||||
|
let virtual_env_prompt = (if ('' | is-empty) {
|
||||||
|
($virtual_env | path basename)
|
||||||
|
} else {
|
||||||
|
''
|
||||||
|
})
|
||||||
|
|
||||||
|
let new_env = {
|
||||||
|
$path_name : $new_path
|
||||||
|
VIRTUAL_ENV : $virtual_env
|
||||||
|
VIRTUAL_ENV_PROMPT : $virtual_env_prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
let new_env = (if (is-env-true 'VIRTUAL_ENV_DISABLE_PROMPT') {
|
||||||
|
$new_env
|
||||||
|
} else {
|
||||||
|
# Creating the new prompt for the session
|
||||||
|
let virtual_prefix = $'(char lparen)($virtual_env_prompt)(char rparen) '
|
||||||
|
|
||||||
|
# Back up the old prompt builder
|
||||||
|
let old_prompt_command = (if (has-env 'PROMPT_COMMAND') {
|
||||||
|
$env.PROMPT_COMMAND
|
||||||
|
} else {
|
||||||
|
''
|
||||||
|
})
|
||||||
|
|
||||||
|
let new_prompt = (if (has-env 'PROMPT_COMMAND') {
|
||||||
|
if 'closure' in ($old_prompt_command | describe) {
|
||||||
|
{|| $'($virtual_prefix)(do $old_prompt_command)' }
|
||||||
|
} else {
|
||||||
|
{|| $'($virtual_prefix)($old_prompt_command)' }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
{|| $'($virtual_prefix)' }
|
||||||
|
})
|
||||||
|
|
||||||
|
$new_env | merge {
|
||||||
|
PROMPT_COMMAND : $new_prompt
|
||||||
|
VIRTUAL_PREFIX : $virtual_prefix
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
# Environment variables that will be loaded as the virtual env
|
||||||
|
load-env $new_env
|
||||||
|
}
|
||||||
|
|
||||||
|
export alias pydoc = python -m pydoc
|
||||||
|
export alias deactivate = overlay hide activate
|
61
crates/gourgeist/src/activator/activate.ps1
Normal file
61
crates/gourgeist/src/activator/activate.ps1
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
$script:THIS_PATH = $myinvocation.mycommand.path
|
||||||
|
$script:BASE_DIR = Split-Path (Resolve-Path "$THIS_PATH/..") -Parent
|
||||||
|
|
||||||
|
function global:deactivate([switch] $NonDestructive) {
|
||||||
|
if (Test-Path variable:_OLD_VIRTUAL_PATH) {
|
||||||
|
$env:PATH = $variable:_OLD_VIRTUAL_PATH
|
||||||
|
Remove-Variable "_OLD_VIRTUAL_PATH" -Scope global
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Test-Path function:_old_virtual_prompt) {
|
||||||
|
$function:prompt = $function:_old_virtual_prompt
|
||||||
|
Remove-Item function:\_old_virtual_prompt
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($env:VIRTUAL_ENV) {
|
||||||
|
Remove-Item env:VIRTUAL_ENV -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($env:VIRTUAL_ENV_PROMPT) {
|
||||||
|
Remove-Item env:VIRTUAL_ENV_PROMPT -ErrorAction SilentlyContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$NonDestructive) {
|
||||||
|
# Self destruct!
|
||||||
|
Remove-Item function:deactivate
|
||||||
|
Remove-Item function:pydoc
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function global:pydoc {
|
||||||
|
python -m pydoc $args
|
||||||
|
}
|
||||||
|
|
||||||
|
# unset irrelevant variables
|
||||||
|
deactivate -nondestructive
|
||||||
|
|
||||||
|
$VIRTUAL_ENV = $BASE_DIR
|
||||||
|
$env:VIRTUAL_ENV = $VIRTUAL_ENV
|
||||||
|
|
||||||
|
if ("" -ne "") {
|
||||||
|
$env:VIRTUAL_ENV_PROMPT = ""
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
$env:VIRTUAL_ENV_PROMPT = $( Split-Path $env:VIRTUAL_ENV -Leaf )
|
||||||
|
}
|
||||||
|
|
||||||
|
New-Variable -Scope global -Name _OLD_VIRTUAL_PATH -Value $env:PATH
|
||||||
|
|
||||||
|
$env:PATH = "$env:VIRTUAL_ENV/bin:" + $env:PATH
|
||||||
|
if (!$env:VIRTUAL_ENV_DISABLE_PROMPT) {
|
||||||
|
function global:_old_virtual_prompt {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
$function:_old_virtual_prompt = $function:prompt
|
||||||
|
|
||||||
|
function global:prompt {
|
||||||
|
# Add the custom prefix to the existing prompt
|
||||||
|
$previous_prompt_value = & $function:_old_virtual_prompt
|
||||||
|
("(" + $env:VIRTUAL_ENV_PROMPT + ") " + $previous_prompt_value)
|
||||||
|
}
|
||||||
|
}
|
36
crates/gourgeist/src/activator/activate_this.py
Normal file
36
crates/gourgeist/src/activator/activate_this.py
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
"""
|
||||||
|
Activate virtualenv for current interpreter:
|
||||||
|
|
||||||
|
Use exec(open(this_file).read(), {'__file__': this_file}).
|
||||||
|
|
||||||
|
This can be used when you must use an existing Python interpreter, not the virtualenv bin/python.
|
||||||
|
""" # noqa: D415
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import site
|
||||||
|
import sys
|
||||||
|
|
||||||
|
try:
|
||||||
|
abs_file = os.path.abspath(__file__)
|
||||||
|
except NameError as exc:
|
||||||
|
msg = "You must use exec(open(this_file).read(), {'__file__': this_file}))"
|
||||||
|
raise AssertionError(msg) from exc
|
||||||
|
|
||||||
|
bin_dir = os.path.dirname(abs_file)
|
||||||
|
base = bin_dir[: -len("bin") - 1] # strip away the bin part from the __file__, plus the path separator
|
||||||
|
|
||||||
|
# prepend bin to PATH (this file is inside the bin directory)
|
||||||
|
os.environ["PATH"] = os.pathsep.join([bin_dir, *os.environ.get("PATH", "").split(os.pathsep)])
|
||||||
|
os.environ["VIRTUAL_ENV"] = base # virtual env is right above bin directory
|
||||||
|
os.environ["VIRTUAL_ENV_PROMPT"] = "" or os.path.basename(base) # noqa: SIM222
|
||||||
|
|
||||||
|
# add the virtual environments libraries to the host python import mechanism
|
||||||
|
prev_length = len(sys.path)
|
||||||
|
for lib in "{{ RELATIVE_SITE_PACKAGES }}".split(os.pathsep):
|
||||||
|
path = os.path.realpath(os.path.join(bin_dir, lib))
|
||||||
|
site.addsitedir(path.decode("utf-8") if "" else path)
|
||||||
|
sys.path[:] = sys.path[prev_length:] + sys.path[0:prev_length]
|
||||||
|
|
||||||
|
sys.real_prefix = sys.prefix
|
||||||
|
sys.prefix = base
|
166
crates/gourgeist/src/bare.rs
Normal file
166
crates/gourgeist/src/bare.rs
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
//! Create a bare virtualenv without any packages install
|
||||||
|
|
||||||
|
use crate::interpreter::InterpreterInfo;
|
||||||
|
use camino::{Utf8Path, Utf8PathBuf};
|
||||||
|
use fs_err as fs;
|
||||||
|
#[cfg(unix)]
|
||||||
|
use fs_err::os::unix::fs::symlink;
|
||||||
|
use fs_err::File;
|
||||||
|
use std::io;
|
||||||
|
use std::io::{BufWriter, Write};
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
/// The bash activate scripts with the venv dependent paths patches out
|
||||||
|
const ACTIVATE_TEMPLATES: &[(&str, &str)] = &[
|
||||||
|
("activate", include_str!("activator/activate")),
|
||||||
|
("activate.csh", include_str!("activator/activate.csh")),
|
||||||
|
("activate.fish", include_str!("activator/activate.fish")),
|
||||||
|
("activate.nu", include_str!("activator/activate.nu")),
|
||||||
|
("activate.ps1", include_str!("activator/activate.ps1")),
|
||||||
|
(
|
||||||
|
"activate_this.py",
|
||||||
|
include_str!("activator/activate_this.py"),
|
||||||
|
),
|
||||||
|
];
|
||||||
|
const VIRTUALENV_PATCH: &str = include_str!("_virtualenv.py");
|
||||||
|
|
||||||
|
/// Very basic `.cfg` file format writer.
|
||||||
|
fn write_cfg(f: &mut impl Write, data: &[(&str, String); 8]) -> io::Result<()> {
|
||||||
|
for (key, value) in data {
|
||||||
|
writeln!(f, "{} = {}", key, value)?;
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Absolute paths of the virtualenv
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct VenvPaths {
|
||||||
|
/// The location of the virtualenv, e.g. `.venv`
|
||||||
|
pub root: Utf8PathBuf,
|
||||||
|
/// The python interpreter.rs inside the virtualenv, on unix `.venv/bin/python`
|
||||||
|
pub interpreter: Utf8PathBuf,
|
||||||
|
/// The directory with the scripts, on unix `.venv/bin`
|
||||||
|
pub bin: Utf8PathBuf,
|
||||||
|
/// The site-packages directory where all the packages are installed to, on unix
|
||||||
|
/// and python 3.11 `.venv/lib/python3.11/site-packages`
|
||||||
|
pub site_packages: Utf8PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Write all the files that belong to a venv without any packages installed.
|
||||||
|
pub fn create_bare_venv(
|
||||||
|
location: &Utf8Path,
|
||||||
|
base_python: &Utf8Path,
|
||||||
|
info: &InterpreterInfo,
|
||||||
|
) -> io::Result<VenvPaths> {
|
||||||
|
if location.exists() {
|
||||||
|
if location.join("pyvenv.cfg").is_file() {
|
||||||
|
info!("Removing existing directory");
|
||||||
|
fs::remove_dir_all(location)?;
|
||||||
|
} else {
|
||||||
|
return Err(io::Error::new(
|
||||||
|
io::ErrorKind::AlreadyExists,
|
||||||
|
format!("The directory {location} exists, but it is not virtualenv"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fs::create_dir_all(location)?;
|
||||||
|
// TODO: I bet on windows we'll have to strip the prefix again
|
||||||
|
let location = location.canonicalize_utf8()?;
|
||||||
|
let bin_dir = {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
location.join("bin")
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
location.join("Bin")
|
||||||
|
}
|
||||||
|
#[cfg(not(any(unix, windows)))]
|
||||||
|
{
|
||||||
|
compile_error!("only unix (like mac and linux) and windows are supported")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
fs::write(location.join(".gitignore"), "*")?;
|
||||||
|
|
||||||
|
// Different names for the python interpreter
|
||||||
|
fs::create_dir(&bin_dir)?;
|
||||||
|
let venv_python = {
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
bin_dir.join("python")
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
bin_dir.join("python.exe")
|
||||||
|
}
|
||||||
|
#[cfg(not(any(unix, windows)))]
|
||||||
|
{
|
||||||
|
compile_error!("only unix (like mac and linux) and windows are supported")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
symlink(base_python, &venv_python)?;
|
||||||
|
symlink("python", bin_dir.join(format!("python{}", info.major)))?;
|
||||||
|
symlink(
|
||||||
|
"python",
|
||||||
|
bin_dir.join(format!("python{}.{}", info.major, info.minor)),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add all the activate scripts for different shells
|
||||||
|
for (name, template) in ACTIVATE_TEMPLATES {
|
||||||
|
let activator = template
|
||||||
|
.replace("{{ VIRTUAL_ENV_DIR }}", location.as_str())
|
||||||
|
.replace(
|
||||||
|
"{{ RELATIVE_SITE_PACKAGES }}",
|
||||||
|
&format!("../lib/python{}.{}/site-packages", info.major, info.minor),
|
||||||
|
);
|
||||||
|
fs::write(bin_dir.join(name), activator)?;
|
||||||
|
}
|
||||||
|
|
||||||
|
// pyvenv.cfg
|
||||||
|
let python_home = base_python
|
||||||
|
.parent()
|
||||||
|
.ok_or_else(|| {
|
||||||
|
io::Error::new(
|
||||||
|
io::ErrorKind::NotFound,
|
||||||
|
"The python interpreter needs to have a parent directory",
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.to_string();
|
||||||
|
let pyvenv_cfg_data = &[
|
||||||
|
("home", python_home),
|
||||||
|
("implementation", "CPython".to_string()),
|
||||||
|
("version_info", info.python_version.clone()),
|
||||||
|
("gourgeist", env!("CARGO_PKG_VERSION").to_string()),
|
||||||
|
// I wouldn't allow this option anyway
|
||||||
|
("include-system-site-packages", "false".to_string()),
|
||||||
|
("base-prefix", info.base_prefix.clone()),
|
||||||
|
("base-exec-prefix", info.base_exec_prefix.clone()),
|
||||||
|
("base-executable", base_python.to_string()),
|
||||||
|
];
|
||||||
|
let mut pyvenv_cfg = BufWriter::new(File::create(location.join("pyvenv.cfg"))?);
|
||||||
|
write_cfg(&mut pyvenv_cfg, pyvenv_cfg_data)?;
|
||||||
|
drop(pyvenv_cfg);
|
||||||
|
|
||||||
|
// TODO: This is different on windows
|
||||||
|
let site_packages = location
|
||||||
|
.join("lib")
|
||||||
|
.join(format!("python{}.{}", info.major, info.minor))
|
||||||
|
.join("site-packages");
|
||||||
|
fs::create_dir_all(&site_packages)?;
|
||||||
|
// Install _virtualenv.py patch.
|
||||||
|
// Frankly no idea what that does, i just copied it from virtualenv knowing that
|
||||||
|
// distutils/setuptools will have their cursed reasons
|
||||||
|
fs::write(site_packages.join("_virtualenv.py"), VIRTUALENV_PATCH)?;
|
||||||
|
fs::write(site_packages.join("_virtualenv.pth"), "import _virtualenv")?;
|
||||||
|
|
||||||
|
Ok(VenvPaths {
|
||||||
|
root: location.to_path_buf(),
|
||||||
|
interpreter: venv_python,
|
||||||
|
bin: bin_dir,
|
||||||
|
site_packages,
|
||||||
|
})
|
||||||
|
}
|
196
crates/gourgeist/src/interpreter.rs
Normal file
196
crates/gourgeist/src/interpreter.rs
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
use crate::{crate_cache_dir, Error};
|
||||||
|
use camino::{FromPathBufError, Utf8Path, Utf8PathBuf};
|
||||||
|
use fs_err as fs;
|
||||||
|
use fs_err::File;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::io;
|
||||||
|
use std::io::{BufReader, Write};
|
||||||
|
use std::process::{Command, Stdio};
|
||||||
|
use std::time::SystemTime;
|
||||||
|
use tracing::{debug, error, info, warn};
|
||||||
|
|
||||||
|
const QUERY_PYTHON: &str = include_str!("query_python.py");
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
pub struct InterpreterInfo {
|
||||||
|
pub base_exec_prefix: String,
|
||||||
|
pub base_prefix: String,
|
||||||
|
pub major: u8,
|
||||||
|
pub minor: u8,
|
||||||
|
pub python_version: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the interpreter.rs info, either cached or by running it.
|
||||||
|
pub fn get_interpreter_info(interpreter: &Utf8Path) -> Result<InterpreterInfo, Error> {
|
||||||
|
let cache_dir = crate_cache_dir()?.join("interpreter_info");
|
||||||
|
|
||||||
|
let index = seahash::hash(interpreter.as_str().as_bytes());
|
||||||
|
let cache_file = cache_dir.join(index.to_string()).with_extension("json");
|
||||||
|
|
||||||
|
let modified = fs::metadata(interpreter)?
|
||||||
|
.modified()?
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.unwrap_or_default()
|
||||||
|
.as_millis();
|
||||||
|
|
||||||
|
if cache_file.exists() {
|
||||||
|
let cache_entry: Result<CacheEntry, String> = File::open(&cache_file)
|
||||||
|
.map_err(|err| err.to_string())
|
||||||
|
.and_then(|cache_reader| {
|
||||||
|
serde_json::from_reader(BufReader::new(cache_reader)).map_err(|err| err.to_string())
|
||||||
|
});
|
||||||
|
match cache_entry {
|
||||||
|
Ok(cache_entry) => {
|
||||||
|
debug!("Using cache entry {cache_file}");
|
||||||
|
if modified == cache_entry.modified && interpreter == cache_entry.interpreter {
|
||||||
|
return Ok(cache_entry.interpreter_info);
|
||||||
|
} else {
|
||||||
|
debug!(
|
||||||
|
"Removing mismatching cache entry {cache_file} ({} {} {} {})",
|
||||||
|
modified, cache_entry.modified, interpreter, cache_entry.interpreter
|
||||||
|
);
|
||||||
|
if let Err(remove_err) = fs::remove_file(&cache_file) {
|
||||||
|
warn!("Failed to mismatching cache file at {cache_file}: {remove_err}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(cache_err) => {
|
||||||
|
debug!("Removing broken cache entry {cache_file} ({cache_err})");
|
||||||
|
if let Err(remove_err) = fs::remove_file(&cache_file) {
|
||||||
|
warn!("Failed to remove broken cache file at {cache_file}: {remove_err} (original error: {cache_err})")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let interpreter_info = query_interpreter(interpreter)?;
|
||||||
|
fs::create_dir_all(&cache_dir)?;
|
||||||
|
let cache_entry = CacheEntry {
|
||||||
|
interpreter: interpreter.to_path_buf(),
|
||||||
|
modified,
|
||||||
|
interpreter_info: interpreter_info.clone(),
|
||||||
|
};
|
||||||
|
let mut cache_writer = File::create(&cache_file)?;
|
||||||
|
serde_json::to_writer(&mut cache_writer, &cache_entry).map_err(io::Error::from)?;
|
||||||
|
|
||||||
|
Ok(interpreter_info)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||||
|
struct CacheEntry {
|
||||||
|
interpreter: Utf8PathBuf,
|
||||||
|
modified: u128,
|
||||||
|
interpreter_info: InterpreterInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Runs a python script that returns the relevant info about the interpreter.rs as json
|
||||||
|
fn query_interpreter(interpreter: &Utf8Path) -> Result<InterpreterInfo, Error> {
|
||||||
|
let mut child = Command::new(interpreter)
|
||||||
|
.stdin(Stdio::piped())
|
||||||
|
.stdout(Stdio::piped())
|
||||||
|
.stderr(Stdio::piped())
|
||||||
|
.spawn()?;
|
||||||
|
|
||||||
|
if let Some(mut stdin) = child.stdin.take() {
|
||||||
|
stdin
|
||||||
|
.write_all(QUERY_PYTHON.as_bytes())
|
||||||
|
.map_err(|err| Error::PythonSubcommand {
|
||||||
|
interpreter: interpreter.to_path_buf(),
|
||||||
|
err,
|
||||||
|
})?;
|
||||||
|
}
|
||||||
|
let output = child.wait_with_output()?;
|
||||||
|
let stdout = String::from_utf8(output.stdout).unwrap_or_else(|err| {
|
||||||
|
// At this point, there was most likely an error caused by a non-utf8 character, so we're in
|
||||||
|
// an ugly case but still very much want to give the user a chance
|
||||||
|
error!(
|
||||||
|
"The stdout of the failed call of the call to {} contains non-utf8 characters",
|
||||||
|
interpreter
|
||||||
|
);
|
||||||
|
String::from_utf8_lossy(err.as_bytes()).to_string()
|
||||||
|
});
|
||||||
|
let stderr = String::from_utf8(output.stderr).unwrap_or_else(|err| {
|
||||||
|
error!(
|
||||||
|
"The stderr of the failed call of the call to {} contains non-utf8 characters",
|
||||||
|
interpreter
|
||||||
|
);
|
||||||
|
String::from_utf8_lossy(err.as_bytes()).to_string()
|
||||||
|
});
|
||||||
|
// stderr isn't technically a criterion for success, but i don't know of any cases where there
|
||||||
|
// should be stderr output and if there is, we want to know
|
||||||
|
if !output.status.success() || !stderr.trim().is_empty() {
|
||||||
|
return Err(Error::PythonSubcommand {
|
||||||
|
interpreter: interpreter.to_path_buf(),
|
||||||
|
err: io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
format!(
|
||||||
|
"Querying python at {} failed with status {}:\n--- stdout:\n{}\n--- stderr:\n{}",
|
||||||
|
interpreter,
|
||||||
|
output.status,
|
||||||
|
stdout.trim(),
|
||||||
|
stderr.trim()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
}
|
||||||
|
let data = serde_json::from_str::<InterpreterInfo>(&stdout).map_err(|err|
|
||||||
|
Error::PythonSubcommand {
|
||||||
|
interpreter: interpreter.to_path_buf(),
|
||||||
|
err: io::Error::new(
|
||||||
|
io::ErrorKind::Other,
|
||||||
|
format!(
|
||||||
|
"Querying python at {} did not return the expected data ({}):\n--- stdout:\n{}\n--- stderr:\n{}",
|
||||||
|
interpreter,
|
||||||
|
err,
|
||||||
|
stdout.trim(),
|
||||||
|
stderr.trim()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)?;
|
||||||
|
Ok(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse the value of the `-p`/`--python` option, which can be e.g. `3.11`, `python3.11`,
|
||||||
|
/// `tools/bin/python3.11` or `/usr/bin/python3.11`.
|
||||||
|
pub fn parse_python_cli(cli_python: Option<Utf8PathBuf>) -> Result<Utf8PathBuf, crate::Error> {
|
||||||
|
let python = if let Some(python) = cli_python {
|
||||||
|
if let Some((major, minor)) = python
|
||||||
|
.as_str()
|
||||||
|
.split_once('.')
|
||||||
|
.and_then(|(major, minor)| Some((major.parse::<u8>().ok()?, minor.parse::<u8>().ok()?)))
|
||||||
|
{
|
||||||
|
if major != 3 {
|
||||||
|
return Err(crate::Error::InvalidPythonInterpreter(
|
||||||
|
"Only python 3 is supported".into(),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
info!("Looking for python {major}.{minor}");
|
||||||
|
Utf8PathBuf::from(format!("python{major}.{minor}"))
|
||||||
|
} else {
|
||||||
|
python
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Utf8PathBuf::from("python3".to_string())
|
||||||
|
};
|
||||||
|
|
||||||
|
// Call `which` to find it in path, if not given a path
|
||||||
|
let python = if python.components().count() > 1 {
|
||||||
|
// Does this path contain a slash (unix) or backslash (windows)? In that case, assume it's
|
||||||
|
// relative or absolute path that we don't need to resolve
|
||||||
|
info!("Assuming {python} is a path");
|
||||||
|
python
|
||||||
|
} else {
|
||||||
|
let python_in_path = which::which(python.as_std_path())
|
||||||
|
.map_err(|err| {
|
||||||
|
crate::Error::InvalidPythonInterpreter(
|
||||||
|
format!("Can't find {python} ({err})").into(),
|
||||||
|
)
|
||||||
|
})?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|err: FromPathBufError| err.into_io_error())?;
|
||||||
|
info!("Resolved {python} to {python_in_path}");
|
||||||
|
python_in_path
|
||||||
|
};
|
||||||
|
Ok(python)
|
||||||
|
}
|
85
crates/gourgeist/src/lib.rs
Normal file
85
crates/gourgeist/src/lib.rs
Normal file
|
@ -0,0 +1,85 @@
|
||||||
|
use crate::bare::create_bare_venv;
|
||||||
|
use camino::{Utf8Path, Utf8PathBuf};
|
||||||
|
use dirs::cache_dir;
|
||||||
|
use interpreter::InterpreterInfo;
|
||||||
|
use std::io;
|
||||||
|
use tempfile::PersistError;
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub use interpreter::{get_interpreter_info, parse_python_cli};
|
||||||
|
|
||||||
|
mod bare;
|
||||||
|
mod interpreter;
|
||||||
|
#[cfg(feature = "install")]
|
||||||
|
mod packages;
|
||||||
|
#[cfg(not(feature = "install"))]
|
||||||
|
mod virtualenv_cache;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum Error {
|
||||||
|
#[error(transparent)]
|
||||||
|
IO(#[from] io::Error),
|
||||||
|
/// It's effectively an io error with extra info
|
||||||
|
#[error(transparent)]
|
||||||
|
Persist(#[from] PersistError),
|
||||||
|
/// Adds url and target path to the io error
|
||||||
|
#[error("Failed to download wheel from {url} to {path}")]
|
||||||
|
WheelDownload {
|
||||||
|
url: String,
|
||||||
|
path: Utf8PathBuf,
|
||||||
|
#[source]
|
||||||
|
err: io::Error,
|
||||||
|
},
|
||||||
|
#[error("Failed to determine python interpreter to use")]
|
||||||
|
InvalidPythonInterpreter(#[source] Box<dyn std::error::Error + Sync + Send>),
|
||||||
|
#[error("Failed to query python interpreter at {interpreter}")]
|
||||||
|
PythonSubcommand {
|
||||||
|
interpreter: Utf8PathBuf,
|
||||||
|
#[source]
|
||||||
|
err: io::Error,
|
||||||
|
},
|
||||||
|
#[cfg(feature = "install")]
|
||||||
|
#[error("Failed to contact pypi")]
|
||||||
|
MinReq(#[from] minreq::Error),
|
||||||
|
#[cfg(feature = "install")]
|
||||||
|
#[error("Failed to install {package}")]
|
||||||
|
InstallWheel {
|
||||||
|
package: String,
|
||||||
|
#[source]
|
||||||
|
err: install_wheel_rs::Error,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn crate_cache_dir() -> io::Result<Utf8PathBuf> {
|
||||||
|
Ok(cache_dir()
|
||||||
|
.and_then(|path| Utf8PathBuf::from_path_buf(path).ok())
|
||||||
|
.ok_or_else(|| io::Error::new(io::ErrorKind::Other, "Couldn't detect cache dir"))?
|
||||||
|
.join(env!("CARGO_PKG_NAME")))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a virtualenv and if not bare, install `wheel`, `pip` and `setuptools`.
|
||||||
|
pub fn create_venv(
|
||||||
|
location: &Utf8Path,
|
||||||
|
base_python: &Utf8Path,
|
||||||
|
info: &InterpreterInfo,
|
||||||
|
bare: bool,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let paths = create_bare_venv(location, base_python, info)?;
|
||||||
|
|
||||||
|
if !bare {
|
||||||
|
#[cfg(feature = "install")]
|
||||||
|
{
|
||||||
|
packages::install_base_packages(location, info, &paths)?;
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "install"))]
|
||||||
|
{
|
||||||
|
virtualenv_cache::install_base_packages(
|
||||||
|
&paths.bin,
|
||||||
|
&paths.interpreter,
|
||||||
|
&paths.site_packages,
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
51
crates/gourgeist/src/main.rs
Normal file
51
crates/gourgeist/src/main.rs
Normal file
|
@ -0,0 +1,51 @@
|
||||||
|
use camino::Utf8PathBuf;
|
||||||
|
use clap::Parser;
|
||||||
|
use gourgeist::{create_venv, get_interpreter_info, parse_python_cli};
|
||||||
|
use std::error::Error;
|
||||||
|
use std::process::ExitCode;
|
||||||
|
use std::time::Instant;
|
||||||
|
use tracing::info;
|
||||||
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
|
use tracing_subscriber::{fmt, EnvFilter};
|
||||||
|
|
||||||
|
#[derive(Parser, Debug)]
|
||||||
|
struct Cli {
|
||||||
|
path: Option<Utf8PathBuf>,
|
||||||
|
#[clap(short, long)]
|
||||||
|
python: Option<Utf8PathBuf>,
|
||||||
|
#[clap(long)]
|
||||||
|
bare: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run() -> Result<(), gourgeist::Error> {
|
||||||
|
let cli = Cli::parse();
|
||||||
|
let location = cli.path.unwrap_or(Utf8PathBuf::from(".venv"));
|
||||||
|
let python = parse_python_cli(cli.python)?;
|
||||||
|
let data = get_interpreter_info(&python)?;
|
||||||
|
create_venv(&location, &python, &data, cli.bare)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> ExitCode {
|
||||||
|
tracing_subscriber::registry()
|
||||||
|
.with(fmt::layer())
|
||||||
|
.with(EnvFilter::from_default_env())
|
||||||
|
.init();
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
let result = run();
|
||||||
|
info!("Took {}ms", start.elapsed().as_millis());
|
||||||
|
if let Err(err) = result {
|
||||||
|
eprintln!("💥 virtualenv creator failed");
|
||||||
|
let mut last_error: Option<&(dyn Error + 'static)> = Some(&err);
|
||||||
|
while let Some(err) = last_error {
|
||||||
|
eprintln!(" Caused by: {}", err);
|
||||||
|
last_error = err.source();
|
||||||
|
}
|
||||||
|
ExitCode::FAILURE
|
||||||
|
} else {
|
||||||
|
ExitCode::SUCCESS
|
||||||
|
}
|
||||||
|
}
|
89
crates/gourgeist/src/packages.rs
Normal file
89
crates/gourgeist/src/packages.rs
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
use crate::bare::VenvPaths;
|
||||||
|
use crate::interpreter::InterpreterInfo;
|
||||||
|
use crate::{crate_cache_dir, Error};
|
||||||
|
use camino::{FromPathBufError, Utf8Path, Utf8PathBuf};
|
||||||
|
use fs_err as fs;
|
||||||
|
use fs_err::File;
|
||||||
|
use install_wheel_rs::{install_wheel, InstallLocation, WheelFilename};
|
||||||
|
#[cfg(feature = "parallel")]
|
||||||
|
use rayon::iter::{IntoParallelIterator, ParallelIterator};
|
||||||
|
use std::io;
|
||||||
|
use std::io::BufWriter;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use tempfile::NamedTempFile;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
|
pub fn download_wheel_cached(filename: &str, url: &str) -> Result<Utf8PathBuf, Error> {
|
||||||
|
let wheels_cache = crate_cache_dir()?.join("wheels");
|
||||||
|
let cached_wheel = wheels_cache.join(filename);
|
||||||
|
if cached_wheel.is_file() {
|
||||||
|
info!("Using cached wheel at {cached_wheel}");
|
||||||
|
return Ok(cached_wheel);
|
||||||
|
}
|
||||||
|
|
||||||
|
info!("Downloading wheel from {url} to {cached_wheel}");
|
||||||
|
fs::create_dir_all(&wheels_cache)?;
|
||||||
|
let mut tempfile = NamedTempFile::new_in(wheels_cache)?;
|
||||||
|
let tempfile_path: Utf8PathBuf = tempfile
|
||||||
|
.path()
|
||||||
|
.to_path_buf()
|
||||||
|
.try_into()
|
||||||
|
.map_err(|err: FromPathBufError| err.into_io_error())?;
|
||||||
|
let mut response = minreq::get(url).send_lazy()?;
|
||||||
|
io::copy(&mut response, &mut BufWriter::new(&mut tempfile)).map_err(|err| {
|
||||||
|
Error::WheelDownload {
|
||||||
|
url: url.to_string(),
|
||||||
|
path: tempfile_path.to_path_buf(),
|
||||||
|
err,
|
||||||
|
}
|
||||||
|
})?;
|
||||||
|
tempfile.persist(&cached_wheel)?;
|
||||||
|
Ok(cached_wheel)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install pip, setuptools and wheel from cache pypi with atm fixed wheels
|
||||||
|
pub fn install_base_packages(
|
||||||
|
location: &Utf8Path,
|
||||||
|
info: &InterpreterInfo,
|
||||||
|
paths: &VenvPaths,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
let install_location = InstallLocation::Venv {
|
||||||
|
venv_base: location.canonicalize()?,
|
||||||
|
python_version: (info.major, info.minor),
|
||||||
|
};
|
||||||
|
let install_location = install_location.acquire_lock()?;
|
||||||
|
|
||||||
|
// TODO: Use the json api instead
|
||||||
|
// TODO: Only check the json API so often (monthly? daily?)
|
||||||
|
let packages = [
|
||||||
|
("pip-23.2.1-py3-none-any.whl", "https://files.pythonhosted.org/packages/50/c2/e06851e8cc28dcad7c155f4753da8833ac06a5c704c109313b8d5a62968a/pip-23.2.1-py3-none-any.whl"),
|
||||||
|
("setuptools-68.2.2-py3-none-any.whl", "https://files.pythonhosted.org/packages/bb/26/7945080113158354380a12ce26873dd6c1ebd88d47f5bc24e2c5bb38c16a/setuptools-68.2.2-py3-none-any.whl"),
|
||||||
|
("wheel-0.41.2-py3-none-any.whl", "https://files.pythonhosted.org/packages/b8/8b/31273bf66016be6ad22bb7345c37ff350276cfd46e389a0c2ac5da9d9073/wheel-0.41.2-py3-none-any.whl"),
|
||||||
|
];
|
||||||
|
#[cfg(feature = "rayon")]
|
||||||
|
let iterator = packages.into_par_iter();
|
||||||
|
#[cfg(not(feature = "rayon"))]
|
||||||
|
let iterator = packages.into_iter();
|
||||||
|
iterator
|
||||||
|
.map(|(filename, url)| {
|
||||||
|
let wheel_file = download_wheel_cached(filename, url)?;
|
||||||
|
let parsed_filename = WheelFilename::from_str(filename).unwrap();
|
||||||
|
install_wheel(
|
||||||
|
&install_location,
|
||||||
|
File::open(wheel_file)?,
|
||||||
|
parsed_filename,
|
||||||
|
false,
|
||||||
|
&[],
|
||||||
|
// Only relevant for monotrail style installation
|
||||||
|
"",
|
||||||
|
paths.interpreter.as_std_path(),
|
||||||
|
)
|
||||||
|
.map_err(|err| Error::InstallWheel {
|
||||||
|
package: filename.to_string(),
|
||||||
|
err,
|
||||||
|
})?;
|
||||||
|
Ok(())
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<()>, Error>>()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
18
crates/gourgeist/src/query_python.py
Normal file
18
crates/gourgeist/src/query_python.py
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import json
|
||||||
|
import sys
|
||||||
|
from platform import python_version
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
data = {
|
||||||
|
"base_exec_prefix": sys.base_exec_prefix,
|
||||||
|
"base_prefix": sys.base_prefix,
|
||||||
|
"major": sys.version_info.major,
|
||||||
|
"minor": sys.version_info.minor,
|
||||||
|
"python_version": python_version(),
|
||||||
|
}
|
||||||
|
print(json.dumps(data))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
109
crates/gourgeist/src/virtualenv_cache.rs
Normal file
109
crates/gourgeist/src/virtualenv_cache.rs
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
//! Deprecated, use only as template when implementing caching
|
||||||
|
|
||||||
|
use crate::Error;
|
||||||
|
use camino::{Utf8Path, Utf8PathBuf};
|
||||||
|
use dirs::data_dir;
|
||||||
|
use fs_err as fs;
|
||||||
|
use std::io;
|
||||||
|
use std::path::Path;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
|
/// Install wheel, pip and setuptools from the cache
|
||||||
|
pub(crate) fn install_base_packages(
|
||||||
|
bin_dir: &Utf8Path,
|
||||||
|
venv_python: &Utf8Path,
|
||||||
|
site_packages: &Utf8Path,
|
||||||
|
) -> Result<(), Error> {
|
||||||
|
// Install packages
|
||||||
|
// TODO: Implement our own logic:
|
||||||
|
// * Our own cache and logic to detect whether a wheel is present
|
||||||
|
// * Check if the version is recent (e.g. update if older than 1 month)
|
||||||
|
// * Query pypi API if no, parse versions (pep440) and their metadata
|
||||||
|
// * Download compatible wheel (py3-none-any should do)
|
||||||
|
// * Install into the cache directory
|
||||||
|
let prefix = "virtualenv/wheel/3.11/image/1/CopyPipInstall/";
|
||||||
|
let wheel_tag = "py3-none-any";
|
||||||
|
let packages = &[
|
||||||
|
("pip", "23.2.1"),
|
||||||
|
("setuptools", "68.2.2"),
|
||||||
|
("wheel", "0.41.2"),
|
||||||
|
];
|
||||||
|
let virtualenv_data_dir: Utf8PathBuf = data_dir().unwrap().try_into().unwrap();
|
||||||
|
for (name, version) in packages {
|
||||||
|
// TODO: acquire lock
|
||||||
|
let unpacked_wheel = virtualenv_data_dir
|
||||||
|
.join(prefix)
|
||||||
|
.join(format!("{name}-{version}-{wheel_tag}"));
|
||||||
|
debug!("Installing {name} by copying from {unpacked_wheel}");
|
||||||
|
copy_dir_all(&unpacked_wheel, site_packages.as_std_path())?;
|
||||||
|
|
||||||
|
// Generate launcher
|
||||||
|
// virtualenv for some reason creates extra entrypoints that we don't
|
||||||
|
// https://github.com/pypa/virtualenv/blob/025e96fbad37f85617364002ae2a0064b09fc984/src/virtualenv/seed/embed/via_app_data/pip_install/base.py#L74-L95
|
||||||
|
let ini_text = fs::read_to_string(
|
||||||
|
site_packages
|
||||||
|
.join(format!("{name}-{version}.dist-info"))
|
||||||
|
.join("entry_points.txt"),
|
||||||
|
)?;
|
||||||
|
let entry_points_mapping = configparser::ini::Ini::new_cs()
|
||||||
|
.read(ini_text)
|
||||||
|
.map_err(|err| format!("{name} entry_points.txt is invalid: {}", err))
|
||||||
|
.unwrap();
|
||||||
|
for (key, value) in entry_points_mapping
|
||||||
|
.get("console_scripts")
|
||||||
|
.cloned()
|
||||||
|
.unwrap_or_default()
|
||||||
|
{
|
||||||
|
let (import_from, function) = value
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|value| value.split_once(':'))
|
||||||
|
.ok_or_else(|| {
|
||||||
|
format!("{name} entry_points.txt {key} has an invalid value {value:?}")
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
let launcher = bin_dir.join(key);
|
||||||
|
let launcher_script = unix_launcher_script(venv_python, import_from, function);
|
||||||
|
fs::write(&launcher, launcher_script)?;
|
||||||
|
// We need to make the launcher executable
|
||||||
|
#[cfg(target_family = "unix")]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
fs::set_permissions(launcher, std::fs::Permissions::from_mode(0o755))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// https://stackoverflow.com/a/65192210/3549270
|
||||||
|
pub fn copy_dir_all(src: impl AsRef<Path>, dst: impl AsRef<Path>) -> io::Result<()> {
|
||||||
|
fs::create_dir_all(&dst)?;
|
||||||
|
for entry in fs::read_dir(src.as_ref())? {
|
||||||
|
let entry = entry?;
|
||||||
|
let ty = entry.file_type()?;
|
||||||
|
if ty.is_dir() {
|
||||||
|
copy_dir_all(entry.path(), dst.as_ref().join(entry.file_name()))?;
|
||||||
|
} else {
|
||||||
|
fs::copy(entry.path(), dst.as_ref().join(entry.file_name()))?;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Template for the console scripts in the `bin` directory
|
||||||
|
pub fn unix_launcher_script(python: &Utf8Path, import_from: &str, function: &str) -> String {
|
||||||
|
format!(
|
||||||
|
r#"#!{python}
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
from {import_from} import {function}
|
||||||
|
if __name__ == '__main__':
|
||||||
|
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
|
||||||
|
sys.exit({function}())
|
||||||
|
"#,
|
||||||
|
python = python,
|
||||||
|
import_from = import_from,
|
||||||
|
function = function
|
||||||
|
)
|
||||||
|
}
|
BIN
crates/gourgeist/static/gourgeist.png
Normal file
BIN
crates/gourgeist/static/gourgeist.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1 MiB |
32
crates/gourgeist/venv_checker.py
Normal file
32
crates/gourgeist/venv_checker.py
Normal file
|
@ -0,0 +1,32 @@
|
||||||
|
from pathlib import Path
|
||||||
|
from subprocess import check_output, check_call
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
project_root = Path(__file__).parent
|
||||||
|
venv_name = ".venv-rs"
|
||||||
|
venv_python = f"{venv_name}/bin/python"
|
||||||
|
venv_pip = f"{venv_name}/bin/pip"
|
||||||
|
|
||||||
|
command = f". {venv_name}/bin/activate && which python"
|
||||||
|
output = check_output(["bash"], input=command, text=True).strip()
|
||||||
|
assert output == str(project_root.joinpath(venv_python)), output
|
||||||
|
|
||||||
|
command = f". {venv_name}/bin/activate && wheel help"
|
||||||
|
output = check_output(["bash"], input=command, text=True).strip()
|
||||||
|
assert output.startswith("usage:"), output
|
||||||
|
|
||||||
|
output = (
|
||||||
|
check_output([venv_python, "imasnake.py"], text=True)
|
||||||
|
.strip()
|
||||||
|
.splitlines()
|
||||||
|
)
|
||||||
|
assert output[0] == str(project_root.joinpath(venv_python)), output
|
||||||
|
assert not output[2].startswith(str(project_root)), output
|
||||||
|
assert output[3] == str(project_root.joinpath(venv_name)), output
|
||||||
|
|
||||||
|
check_call([venv_pip, "install", "tqdm"])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
Loading…
Add table
Add a link
Reference in a new issue