Skip to content

Commit 08ea6ad

Browse files
committed
install packages from host machine instead of from containers
1 parent d04f22c commit 08ea6ad

File tree

2 files changed

+109
-6
lines changed

2 files changed

+109
-6
lines changed

tutor/commands/jobs.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -573,7 +573,7 @@ def pip_install(context: click.Context, package: str) -> t.Iterable[tuple[str, s
573573
yield ("lms", script)
574574

575575

576-
@click.command(help="Remove an Xblock")
576+
@click.command(help="Remove a pip package at runtime")
577577
@click.pass_obj
578578
@click.pass_context
579579
@click.argument("package")
@@ -598,7 +598,7 @@ def pip_uninstall(
598598

599599
script = "rm -rf /mnt/persistent-python-packages/lib/"
600600
config = tutor_config.load(context.root)
601-
values = config["PERSISTENT_PIP_PACKAGES"]
601+
values = t.cast(list[str], config["PERSISTENT_PIP_PACKAGES"])
602602
remaining_packages = " ".join(values)
603603
if len(values) > 0:
604604
script += f" && pip install --prefix=/mnt/persistent-python-packages {remaining_packages}"

tutor/commands/packages.py

Lines changed: 107 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,26 @@
11
import click
2+
import os
3+
import shlex
4+
import shutil
5+
import subprocess
6+
from typing import cast
27

38
from tutor import config as tutor_config
4-
from tutor import fmt
59
from tutor.commands.context import Context
10+
from tutor.commands.config import save as config_save_command
11+
12+
13+
HERE = os.path.abspath(os.path.dirname(__file__))
14+
DEPS_ZIP_PATH = os.path.join(HERE, "deps.zip")
15+
DEPS_PATH = os.path.join(HERE, "deps/")
616

717

818
@click.group(
919
name="packages",
1020
short_help="Manage packages",
1121
)
1222
def packages_command() -> None:
13-
""" """
23+
"""Custom persistent package manager for Tutor."""
1424

1525

1626
@click.command(name="list")
@@ -21,10 +31,103 @@ def list_command(context: Context) -> None:
2131
2232
Entries will be fetched from the `PERSISTENT_PIP_PACKAGES` config setting.
2333
"""
34+
config = tutor_config.load(context.root)
35+
packages = [
36+
package for package in cast(list[str], config["PERSISTENT_PIP_PACKAGES"])
37+
]
38+
print(packages)
2439

40+
41+
@click.command(name="build")
42+
@click.pass_obj
43+
def build(context: Context) -> None:
44+
"""Rebuild dependencies from scratch."""
2545
config = tutor_config.load(context.root)
26-
packages = [package for package in config["PERSISTENT_PIP_PACKAGES"]]
27-
fmt.echo(sorted(packages))
46+
DEPS = cast(list[str], config["PERSISTENT_PIP_PACKAGES"])
47+
48+
build_dir = f"{context.root}/data/persistent-python-packages"
49+
50+
lib_path = os.path.join(build_dir, "lib")
51+
bin_path = os.path.join(build_dir, "bin")
52+
53+
if os.path.exists(lib_path):
54+
shutil.rmtree(lib_path)
55+
if os.path.exists(bin_path):
56+
shutil.rmtree(bin_path)
57+
58+
_pip_install(DEPS, build_dir)
59+
60+
61+
@click.command(name="append")
62+
@click.pass_obj
63+
@click.pass_context
64+
@click.argument("package")
65+
def append(click_context: click.Context, context: Context, package: str) -> None:
66+
"""Append a new package to the list."""
67+
click_context.invoke(
68+
config_save_command,
69+
interactive=False,
70+
set_vars=[],
71+
append_vars=[("PERSISTENT_PIP_PACKAGES", package)],
72+
remove_vars=[],
73+
unset_vars=[],
74+
env_only=False,
75+
clean_env=False,
76+
)
77+
78+
build_dir = f"{context.root}/data/persistent-python-packages"
79+
80+
if os.path.exists(DEPS_ZIP_PATH):
81+
shutil.unpack_archive(DEPS_ZIP_PATH, extract_dir=build_dir)
82+
83+
_pip_install([package], build_dir)
84+
85+
86+
@click.command(name="remove")
87+
@click.pass_context
88+
@click.argument("package")
89+
def remove(context: click.Context, package: str) -> None:
90+
"""Remove a package and rebuild archive from scratch."""
91+
context.invoke(
92+
config_save_command,
93+
interactive=False,
94+
set_vars=[],
95+
append_vars=[],
96+
remove_vars=[("PERSISTENT_PIP_PACKAGES", package)],
97+
unset_vars=[],
98+
env_only=False,
99+
clean_env=False,
100+
)
101+
102+
context.invoke(build)
103+
104+
105+
def _pip_install(deps: list[str], prefix_dir: str) -> None:
106+
for dep in deps:
107+
# We use python3.11 because that's whats used in the Dockerfile
108+
check_call(
109+
"python3.11",
110+
"-m",
111+
"pip",
112+
"install",
113+
"--no-deps",
114+
f"--prefix={prefix_dir}",
115+
dep,
116+
)
117+
check_call(f'touch "{prefix_dir}/.uwsgi_trigger"', shell=True)
118+
119+
120+
def check_call(*args: str, shell: bool = False) -> None:
121+
if shell:
122+
command = " ".join(args)
123+
print(command)
124+
subprocess.check_call(command, shell=True)
125+
else:
126+
print(shlex.join(args))
127+
subprocess.check_call(args)
28128

29129

30130
packages_command.add_command(list_command)
131+
packages_command.add_command(build)
132+
packages_command.add_command(append)
133+
packages_command.add_command(remove)

0 commit comments

Comments
 (0)