diff --git a/src/dstack/_internal/server/services/jobs/configurators/base.py b/src/dstack/_internal/server/services/jobs/configurators/base.py index 0e770b2e90..72821f1dd3 100644 --- a/src/dstack/_internal/server/services/jobs/configurators/base.py +++ b/src/dstack/_internal/server/services/jobs/configurators/base.py @@ -220,6 +220,17 @@ def _dstack_image_commands(self) -> List[str]: ): return [] return [ + f"eval $(echo 'export DSTACK_VENV_DIR={DSTACK_DIR}/venv' | sudo tee -a {DSTACK_PROFILE_PATH})", + # Make sure /dstack/venv is owned by the current user. + # XXX: Generally, /dstack and all its descendants should be owned by root, as it is + # intended to be a place for files shared by all users, but since a non-root user + # should be able to install packages via pip and we want to avoid cluttering the user's + # home dir if possible, we make the venv dir owned by the current user rather than + # creating it inside the user's home or (even worse) making /dstack/venv + # world-writable. + "sudo rm -rf $DSTACK_VENV_DIR", + "sudo mkdir $DSTACK_VENV_DIR", + "sudo chown $(id -u):$(id -g) $DSTACK_VENV_DIR", # `uv` may emit: # > warning: `VIRTUAL_ENV=/dstack/venv` does not match the project environment path # > `.venv` and will be ignored; use `--active` to target the active environment @@ -228,9 +239,8 @@ def _dstack_image_commands(self) -> List[str]: # used for legacy `pip`-based configurations). `--no-active` suppresses the warning. # Alternatively, the user can call `deactivate` once before using `uv`. # If the user really wants to reuse dstack's venv, they must spefify `--active`. - f"uv venv -q --prompt dstack -p {self._python()} --seed {DSTACK_DIR}/venv", - f"echo '. {DSTACK_DIR}/venv/bin/activate' >> {DSTACK_PROFILE_PATH}", - f". {DSTACK_DIR}/venv/bin/activate", + f"uv venv -q --prompt dstack -p {self._python()} --seed $DSTACK_VENV_DIR", + f"eval $(echo '. $DSTACK_VENV_DIR/bin/activate' | sudo tee -a {DSTACK_PROFILE_PATH})", ] def _app_specs(self) -> List[AppSpec]: diff --git a/src/tests/_internal/server/routers/test_runs.py b/src/tests/_internal/server/routers/test_runs.py index 5f5037c79d..4f3ab2ed2d 100644 --- a/src/tests/_internal/server/routers/test_runs.py +++ b/src/tests/_internal/server/routers/test_runs.py @@ -115,9 +115,12 @@ def get_dev_env_run_plan_dict( "-i", "-c", ( - "uv venv -q --prompt dstack -p 3.13 --seed /dstack/venv" - " && echo '. /dstack/venv/bin/activate' >> /dstack/profile" - " && . /dstack/venv/bin/activate" + "eval $(echo 'export DSTACK_VENV_DIR=/dstack/venv' | sudo tee -a /dstack/profile)" + " && sudo rm -rf $DSTACK_VENV_DIR" + " && sudo mkdir $DSTACK_VENV_DIR" + " && sudo chown $(id -u):$(id -g) $DSTACK_VENV_DIR" + " && uv venv -q --prompt dstack -p 3.13 --seed $DSTACK_VENV_DIR" + " && eval $(echo '. $DSTACK_VENV_DIR/bin/activate' | sudo tee -a /dstack/profile)" " && (echo 'pip install ipykernel...'" " && pip install -q --no-cache-dir ipykernel 2> /dev/null)" " || echo 'no pip, ipykernel was not installed'" @@ -344,9 +347,12 @@ def get_dev_env_run_dict( "-i", "-c", ( - "uv venv -q --prompt dstack -p 3.13 --seed /dstack/venv" - " && echo '. /dstack/venv/bin/activate' >> /dstack/profile" - " && . /dstack/venv/bin/activate" + "eval $(echo 'export DSTACK_VENV_DIR=/dstack/venv' | sudo tee -a /dstack/profile)" + " && sudo rm -rf $DSTACK_VENV_DIR" + " && sudo mkdir $DSTACK_VENV_DIR" + " && sudo chown $(id -u):$(id -g) $DSTACK_VENV_DIR" + " && uv venv -q --prompt dstack -p 3.13 --seed $DSTACK_VENV_DIR" + " && eval $(echo '. $DSTACK_VENV_DIR/bin/activate' | sudo tee -a /dstack/profile)" " && (echo 'pip install ipykernel...'" " && pip install -q --no-cache-dir ipykernel 2> /dev/null)" " || echo 'no pip, ipykernel was not installed'" diff --git a/src/tests/_internal/server/services/jobs/configurators/test_task.py b/src/tests/_internal/server/services/jobs/configurators/test_task.py index 6397c40e2c..3c80bf226f 100644 --- a/src/tests/_internal/server/services/jobs/configurators/test_task.py +++ b/src/tests/_internal/server/services/jobs/configurators/test_task.py @@ -98,10 +98,15 @@ async def test_with_commands_no_image(self, shell: Optional[str], expected_shell expected_shell, "-i", "-c", - "uv venv -q --prompt dstack -p 3.12 --seed /dstack/venv" - " && echo '. /dstack/venv/bin/activate' >> /dstack/profile" - " && . /dstack/venv/bin/activate" - " && sleep inf", + ( + "eval $(echo 'export DSTACK_VENV_DIR=/dstack/venv' | sudo tee -a /dstack/profile)" + " && sudo rm -rf $DSTACK_VENV_DIR" + " && sudo mkdir $DSTACK_VENV_DIR" + " && sudo chown $(id -u):$(id -g) $DSTACK_VENV_DIR" + " && uv venv -q --prompt dstack -p 3.12 --seed $DSTACK_VENV_DIR" + " && eval $(echo '. $DSTACK_VENV_DIR/bin/activate' | sudo tee -a /dstack/profile)" + " && sleep inf" + ), ] async def test_no_commands(self, image_config_mock: ImageConfig):