11import click
2+ import os
3+ import shlex
4+ import shutil
5+ import subprocess
26
37from tutor import config as tutor_config
48from tutor import fmt
59from tutor .commands .context import Context
10+ from tutor .commands .config import save as config_save_command
611
712
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/" )
16+
817@click .group (
918 name = "packages" ,
1019 short_help = "Manage packages" ,
1120)
1221def packages_command () -> None :
13- """ """
14-
22+ """Custom persistent package manager for Tutor."""
1523
1624@click .command (name = "list" )
1725@click .pass_obj
@@ -21,10 +29,93 @@ def list_command(context: Context) -> None:
2129
2230 Entries will be fetched from the `PERSISTENT_PIP_PACKAGES` config setting.
2331 """
24-
2532 config = tutor_config .load (context .root )
2633 packages = [package for package in config ["PERSISTENT_PIP_PACKAGES" ]]
2734 fmt .echo (sorted (packages ))
2835
2936
37+ @click .command (name = "build" )
38+ @click .pass_obj
39+ def build (context : Context ):
40+ """Rebuild dependencies from scratch."""
41+ config = tutor_config .load (context .root )
42+ DEPS = config ["PERSISTENT_PIP_PACKAGES" ]
43+
44+ build_dir = f"{ context .root } /data/persistent-python-packages"
45+
46+ lib_path = os .path .join (build_dir , "lib" )
47+ bin_path = os .path .join (build_dir , "bin" )
48+
49+ if os .path .exists (lib_path ):
50+ shutil .rmtree (lib_path )
51+ if os .path .exists (bin_path ):
52+ shutil .rmtree (bin_path )
53+
54+ _pip_install (DEPS , build_dir )
55+
56+
57+ @click .command (name = "append" )
58+ @click .pass_obj
59+ @click .pass_context
60+ @click .argument ("package" )
61+ def append (click_context : click .Context , context : Context , package : str ):
62+ """Append a new package to the list."""
63+ click_context .invoke (
64+ config_save_command ,
65+ interactive = False ,
66+ set_vars = [],
67+ append_vars = [("PERSISTENT_PIP_PACKAGES" , package )],
68+ remove_vars = [],
69+ unset_vars = [],
70+ env_only = False ,
71+ clean_env = False ,
72+ )
73+
74+ build_dir = f"{ context .root } /data/persistent-python-packages"
75+
76+ if os .path .exists (DEPS_ZIP_PATH ):
77+ shutil .unpack_archive (DEPS_ZIP_PATH , extract_dir = build_dir )
78+
79+ _pip_install ([package ], build_dir )
80+
81+
82+ @click .command (name = "remove" )
83+ @click .pass_context
84+ @click .argument ("package" )
85+ def remove (context : click .Context , package : str ):
86+ """Remove a package and rebuild archive from scratch."""
87+ context .invoke (
88+ config_save_command ,
89+ interactive = False ,
90+ set_vars = [],
91+ append_vars = [],
92+ remove_vars = [("PERSISTENT_PIP_PACKAGES" , package )],
93+ unset_vars = [],
94+ env_only = False ,
95+ clean_env = False ,
96+ )
97+
98+ context .invoke (build )
99+
100+
101+ def _pip_install (deps , prefix_dir ):
102+ for dep in deps :
103+ # We use python3.11 because that's whats used in the Dockerfile
104+ check_call ("python3.11" , "-m" , "pip" , "install" , "--no-deps" , f"--prefix={ prefix_dir } " , dep )
105+ check_call (f'touch "{ prefix_dir } /.uwsgi_trigger"' , shell = True )
106+
107+
108+ def check_call (* args , shell = False ):
109+ if shell :
110+ command = " " .join (args )
111+ print (command )
112+ subprocess .check_call (command , shell = True )
113+ else :
114+ print (shlex .join (args ))
115+ subprocess .check_call (args )
116+
117+
30118packages_command .add_command (list_command )
119+ packages_command .add_command (build )
120+ packages_command .add_command (append )
121+ packages_command .add_command (remove )
0 commit comments