prefix-dev / pixi

Package management made easy
https://pixi.sh
BSD 3-Clause "New" or "Revised" License
2.49k stars 140 forks source link

Lifecycle hooks #1183

Open adamblake opened 3 months ago

adamblake commented 3 months ago

Problem description

There was a brief proposal regarding lifecycle hooks in #524 but that issue was framed around Python dependencies. With the addition of the new pypi-dependencies the issue was more or less discarded. However there are other common use-cases for lifecycle hooks, especially when using pixi to setup development environments. Here's an example that will probably look familiar:

[dependencies]
pre-commit = ">=3.7.0,<3.8"

[tasks]
postinstall = "pre-commit install"

Love this project, and always looking forward to the new features. The pypi-dependencies addition has seriously made life easier!

adamblake commented 3 months ago

I also just saw that this is mentioned as one of the requests in #1128

tdejager commented 3 months ago

I was thinking about this a bit, do you think that instead of like a 'postinstall' we can create a trigger that uses the hash of a file to automatically trigger a task after a or a number of tasks.

Think of having both a Python project that uses npm as well. You could make a "trigger" that triggers when the package.json changes after an install or maybe any other task.

Might be more powerful than a postinstall.

adamblake commented 3 months ago

I think that it could a good addition to the idea. It definitely would make this a more complex feature to implement though. If it went that way I would think it could be some kind of configurable trigger, e.g.

[tasks]
list-files = "ls -lah"
reset = "chartpress --reset"

[triggers]
post-install = ["list-files"]

[file-triggers]
"pixi.toml" = ["reset", "list-files"]
ruben-arts commented 3 months ago

What about adding it to the task definition:

[tasks]
install-npm = { cmd = "npm install", trigger = "post-install", inputs = ["package.json", "src/javascript/*.js" }

Where those triggers are predefined names that pixi looks for when it runs the install. These could be extended with more triggers, but I would like to add them based on use-case/on-request.

This would allow you to do something similar to the life cycle scripts from npm but without predefined names for the tasks.

pavelzw commented 3 months ago

Actually, the postinstall = "pip install --no-deps --editable --no-build-isolation ." problem is not quite solved for me yet (because adding pypi-dependencies introduces more complexity into conda-only projects due to additional uv calls which i would like to avoid). Maybe just making postinstall a lifecycle script could solve this issue for me.

bollwyvl commented 3 months ago

Having now tried a few different pixi things: it seems like the task primitive is probably enough for just about everything, where lock and install are just hidden, special cases... making them even more special (or introducing more special tasks) would likely complicate things further.

I've ended up with a pattern like this for a monorepo where i can't use pixi/uv-managed editable installs:

[feature.pip.tasks.install-editable]
inputs = ["pyproject.toml", "contrib/*/pyproject.toml", "editable.txt"]
outputs = ["build/pip-freeze/$PIXI_ENVIRONMENT_NAME.txt"]
cmd = """
  rm -f build/pip-freeze/$PIXI_ENVIRONMENT_NAME.txt
  && python -m pip install -vv --no-deps --no-build-isolation --ignore-installed -r editable.txt
  && mkdir -p build/pip-freeze
  && pip freeze > build/pip-freeze/$PIXI_ENVIRONMENT_NAME.txt"""

... and then have any downstream tasks depends_on: ["install-editable"], with an input of the pip freeze output. This ensures if some packaging-related thing changes (e.g. entry_points), the env in question will be brought up-to-date before trying to test it.

This still gets a little annoying further down the line when the -e argument isn't enough to fully specify the required intermediate environments... indeed, consider a fan-in reporting use case which still needs an editable install:

[feature.docs.tasks.report]
depends_on = [
  "install-editable", # where the current -e will be right
  {task="test", environment="some-env"},
  {task="test", environment="some-other-env"},
]
inputs = [
  "build/pip-freeze/$PIXI_ENVIRONMENT_NAME.txt", 
  "build/reports/some-env.txt", 
  "build/reports/some-other-env.txt"
]

Running pixi run report should then end up running install-editable (up to) three times, each time in a different env (which it may have also created). If the cache data is right, these might have been on entirely separate machines in CI.

Alternately, as the task name regex is luckily already quite tight, a separator could be introduced:

depends_on = ["install-editable", "some-env::test", "some-other-env::test"]

Of course, if inputs/outputs already defined the task-task relationship, and only one task was permitted to outputs a particular file, there would be no need for duplicating or specifying depends_on at all (see #1139).

nicornk commented 3 months ago

I have the need for this as well. I want to install a binary (that is not available in conda-forge/pypi) manually using curl into my environment/bin after pixi is finished building it. Reading this thread makes me think that triggering this automaticaly is not possible today?

tdejager commented 3 months ago

@nicornk if building is task, you can achieve this with a depends_on and a correct input/output. If I understand what you are saying.

nicornk commented 3 months ago

I was referring to calling „pixi install“.

tdejager commented 3 months ago

Ah I understand, that would be the requested feature indeed.

adamblake commented 3 months ago

I have the need for this as well. I want to install a binary (that is not available in conda-forge/pypi) manually using curl into my environment/bin after pixi is finished building it. Reading this thread makes me think that triggering this automaticaly is not possible today?

This is basically one of my use-cases as well --- I want to install a Helm plugin after install, and to do the pre-commit startups and stuff like that. @tdejager maybe it could just be as simple as exposing the "install" as a task that can be depended on (possibly namespacing it to avoid collisions with user-named task install)?