Skip to content

Conversation

@lllangWV
Copy link

Description

Allow users to define dependencies, tasks, and other feature configuration directly on environments using [environments.dev.dependencies] syntax instead of requiring separate feature definitions.

When inline config is detected, a synthetic feature is created with the same name as the environment and prepended to the environment's feature list.

Example usage:

[environments.dev.dependencies]
pytest = "*"

[environments.dev.tasks]
test = "pytest"

This is equivalent to:

[feature.dev.dependencies]
pytest = "*"

[feature.dev.tasks]
test = "pytest"

[environments]
dev = ["dev"]

Fixes #5317

How Has This Been Tested?

Unit Tests (cargo test -p pixi_manifest environment):

  • Parse inline dependencies in environment
  • Parse inline tasks in environment
  • Parse inline with explicit features
  • Validate no features required when inline config present
  • Conflict detection with same-named feature
  • Synthetic feature creation and environment reference

Integration Tests (cargo test -p pixi --test integration_rust inline_environment):

  • test_inline_environment_dependencies - Verify parsing and synthetic feature creation
  • test_inline_environment_with_explicit_features - Combining inline config with explicit features
  • test_inline_environment_tasks - Task parsing in inline config
  • test_inline_environment_lock_file - End-to-end solve with inline dependencies

Linting: pixi run lint passes (includes clippy)
pixi tests: pixi run test produced some failing tests. I was unsure if this is related to the changes here

(373 durations < 0.005s hidden. Use -vv to show these durations.)
================================================================================= short test summary info ==================================================================================
FAILED tests/integration_python/pixi_global/test_shortcuts.py::test_install_simple - AssertionError: Shortcut 'pixi-editor' should exist on linux-64
FAILED tests/integration_python/pixi_global/test_shortcuts.py::test_sync_empty_shortcut_list - AssertionError: Shortcut 'pixi-editor' should exist on linux-64
FAILED tests/integration_python/pixi_global/test_shortcuts.py::test_sync_creation_and_removal - AssertionError: Shortcut 'pixi-editor' should exist on linux-64
FAILED tests/integration_python/pixi_global/test_shortcuts.py::test_update_installs_new_shortcuts - AssertionError: Shortcut 'pixi-editor' should exist on linux-64
FAILED tests/integration_python/pixi_global/test_shortcuts.py::test_sync_removing_environment - AssertionError: Shortcut 'pixi-editor' should exist on linux-64
FAILED tests/integration_python/pixi_global/test_shortcuts.py::test_remove_shortcut - AssertionError: Shortcut 'pixi-editor' should exist on linux-64
FAILED tests/integration_python/pixi_global/test_shortcuts.py::test_add_shortcut - AssertionError: Shortcut 'pixi-editor' should exist on linux-64
======================================================================== 7 failed, 193 passed, 1 xfailed in 40.89s =========================================================================

AI Disclosure

  • This PR contains AI-generated content.
    • I have tested any AI-generated content in my PR.
    • I take responsibility for any AI-generated content in my PR.

Tools: Claude Code with Claude Opus 4.5

Process:

  1. Used the slash command https://raw.githubusercontent.com/humanlayer/humanlayer/refs/heads/main/.claude/commands/research_codebase.md with the issue as the prompt. This identifies
    where in the codebase changes would need to be made if this feature were to be implemented.
  2. Had a back-and-forth to answer questions about the implementation, discussing design decisions documented in the research document. (
    2026-01-24-environments-direct-dependencies.md
    )
  3. Used the slash command https://github.com/humanlayer/blob/main/.claude/commands/create_plan.md to create a detailed implementation plan defining what we're changing, what we're not changing, and how we're testing. (
    2026-01-24-environments-direct-dependencies.md
    )
  4. Used the slash command https://github.com/humanlayer/humanlayer/blob/main/.claude/commands/implement_plan.md to implement all phases in the implementation plan.
  5. Made the model adhere to all requirements in CONTRIBUTING.md.

Checklist:

""

  • I have performed a self-review of my own code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • I have added sufficient tests to cover my changes. See above
  • I have verified that changes that would impact the JSON schema have been made in schema/model.py.

Allow users to define dependencies, tasks, and other feature configuration
directly on environments using `[environments.dev.dependencies]` syntax
instead of requiring separate feature definitions.

When inline config is detected, a synthetic feature is created with the same
name as the environment and prepended to the environment's feature list.

Example usage:
```toml
[environments.dev.dependencies]
pytest = "*"

[environments.dev.tasks]
test = "pytest"
```

This is equivalent to:
```toml
[feature.dev.dependencies]
pytest = "*"

[feature.dev.tasks]
test = "pytest"

[environments]
dev = ["dev"]
```

Closes prefix-dev#5317
@ruben-arts
Copy link
Contributor

Thank you @lllangWV, this is a big change to the manifest. I would like to test run this for a little bit.

One question, that I know @Hofer-Julian has an opinion on, what happens with the "default" feature? Is it still automatically included?

@Hofer-Julian
Copy link
Contributor

One question, that I know @Hofer-Julian has an opinion on, what happens with the "default" feature? Is it still automatically included?

I think we should include it for now. Otherwise it would be a breaking change and that should be done separately

When inline config is detected, a synthetic feature is created with the same name as the environment and prepended to the environment's feature list.

This worries me a bit. Users should still be allowed to define features of the same name as the environment. Also I think creating a feature is not the way to go. My feeling is that this should be handled separately also to avoid clashes with other tools that deal with features

@lllangWV
Copy link
Author

big change to the manifest. I would like to test run this for a little bit.

@ruben-arts Yeah, I figured. My goal was to at least get this discussion going with an initial attempt at this. Let me know if you find any issues.

And, yes currently the default feature it is automatically included. I wanted to try to stick with the original behavior as much as possible

This worries me a bit. Users should still be allowed to define features of the same name as the environment. Also I think creating a feature is not the way to go. My feeling is that this should be handled separately also to avoid clashes with other tools that deal with features

@Hofer-Julian , I see your point. I think I misunderstood your original intention. When I made these changes the intention was that you can use the dependencies defined in environment as another feature in itself.

This synthetic feature approach can create potential confusion:

  1. Naming conflicts: Like you mentioned, users should be able to define [feature.dev] independently of [environments.dev]
  2. Ambiguous sharing semantics: If environment dev has inline config + references feature python, and another environment tries to import the synthetic "dev" feature, they only get the inline dependencies - not python. This could be surprising:
[feature.python.dependencies]                                                                                                                                                                                                                                   
python = "3.14.*"                                                                                                                                                                                                                                               
                                                                                                                                                                                                                                                                
[environments.dev]                                                                                                           
features = ["python"]                                                                                           
                                                                                                                                                                                                                                                                
[environments.dev.dependencies]                                                                                                                                                                                                                                 
pytest = "*"                                                                      
                                                                                                                                                                                                                                                                
[environments.test]                                                                                                                                                                                                                                             
features = ["dev"]  # User might expect python + pytest, but only gets pytest                                                                                                                         
  1. Tooling conflicts: Other tools that inspect features won't distinguish between user-defined and synthetic features

QUESTION:
Should all feature-like fields be supported inline on environments? Currently the implementation allows:

  • dependencies, pypi-dependencies, dev
  • tasks
  • activation
  • target (platform-specific config)
  • channels, channel-priority
  • pypi-options
  • system-requirements

Or should we limit inline config to just dependencies/tasks for simplicity, keeping the more advanced configuration (like system-requirements, pypi-options) as feature-only?

Proposed changes

I guess the change should be instead of creating synthetic features, handle inline environment config as a separate, non-shareable concept. Inline dependencies are "private" to that environment and cannot be referenced by other environments.

This is the rough outline for the changes I would make from the current implementation:

  1. Remove the synthetic feature creation
  2. Store inline config separately on the environment
  3. Remove the conflict check with same-named features
  4. Merge inline config with feature dependencies at solve time

QUESTION:

Does this align more closely with what you want? Let me know your thoughts!

@ruben-arts
Copy link
Contributor

Or should we limit inline config to just dependencies/tasks for simplicity

I was actually most excited and convinced of this feature because of the additional config. So I would like to keep this in.

What would be the logic on merging the configurations?

[feature.test.tasks]
test = "pytest"

[environments.dev]
tasks = { test = "python blabla" }
features = ["test"]

Which contents of the test task will be selected? I think it should be the one in the environments.dev.tasks. To reason about it I don't think it's to strange as to implement it as a feature. Making "this thing" the highest priority feature.

I guess it could be a "special" type of feature that uses all the same logic but is always the highest prio one and is not exposed to the user in any way. I worry that we implement the exact same logic as the features and then sturgle to maintain the two code paths.

@lllangWV
Copy link
Author

What would be the logic on merging the configurations?

Exactly what you said It should be the one in the environments.dev.tasks and this has the highest priority.

I guess it could be a "special" type of feature that uses all the same logic but is always the highest priority one and is not exposed to the user in any way.

I would agree with this. If you do make it public it might cause the issues I mentioned in the earlier comment: "Naming conflicts between a feature with the same name" and "Ambiguous sharing semantics. Is it sharing all the features apart of the environment or just the "private" feature. A user might get confused by this."

I worry that we implement the exact same logic as the features and then sturgle to maintain the two code paths.

Exactly this! I felt bad about explicitly defining the feature fields onto the TomlEnvironemnt because any changes to what a TomlFeature is this would require an update to be compatible. I couldn't think of a better way to do it.

pub struct TomlEnvironment {
    pub features: Option<Spanned<Vec<Spanned<String>>>>,
    pub solve_group: Option<String>,
    pub no_default_feature: bool,
    pub platforms: Option<Spanned<IndexSet<Platform>>>,
    pub channels: Option<Vec<TomlPrioritizedChannel>>,
    pub channel_priority: Option<ChannelPriority>,
    pub solve_strategy: Option<SolveStrategy>,
    pub system_requirements: SystemRequirements,
    pub dependencies: Option<PixiSpanned<UniquePackageMap>>,
    pub host_dependencies: Option<PixiSpanned<UniquePackageMap>>,
    pub build_dependencies: Option<PixiSpanned<UniquePackageMap>>,
    pub pypi_dependencies: Option<IndexMap<PypiPackageName, PixiPypiSpec>>,
    pub dev: Option<IndexMap<rattler_conda_types::PackageName, pixi_spec::TomlLocationSpec>>,
    pub activation: Option<Activation>,
    pub tasks: HashMap<TaskName, Task>,
    pub target: IndexMap<PixiSpanned<TargetSelector>, TomlTarget>,
    pub pypi_options: Option<PypiOptions>,
    pub warnings: Vec<Warning>,

Maybe a "solution" would look something like this. (This is probably wrong with how you would define this formally)

pub struct TomlEnvironment {
    pub features: Option<Spanned<Vec<Spanned<String>>>>,
    pub solve_group: Option<String>,
    pub no_default_feature: bool,
    pub feature Option<TomlFeature>

However, I think the syntax would be "goofy"

[feature.test.tasks]
test = "pytest"
[environments.dev]
features = ["python"]
feature.dev.dependencies = { ruff = "*" }

But maybe this is a non-issue. I couldn't think of a way to reuse the TomlFeature and flatten its field out.

I was going back and forth with claude code about this design issue. I thought I would share its suggestion to assist in the discussion.

You can't escape explicitly listing the fields somewhere if you want flat TOML syntax with toml_span's     
  manual deserialization.                                                                                                  
                                                                                                                           
  The options are:                                                                                                         
                                                                                                                           
  1. Current approach: Fields on TomlEnvironment, manual into_toml_feature() mapping                                       
  2. Helper struct: Fields on FeatureFields, then construct both TomlFeature and TomlEnvironment from it                   
  3. Embed after parsing: Parse fields explicitly, then construct an embedded TomlFeature immediately during               
  deserialization                                                                                                          
                                                                                                                           
  Option 3 might be a slight improvement:                                                                                  
                                                                                                                           
  pub struct TomlEnvironment {                                                                                             
      pub features: Option<Spanned<Vec<Spanned<String>>>>,                                                                 
      pub solve_group: Option<String>,                                                                                     
      pub no_default_feature: bool,                                                                                        
      pub inline: Option<TomlFeature>,  // constructed during deserialize                                                  
  }                                                                                                                        
                                                                                                                           
  In deserialize(), after parsing all the feature fields, you'd construct the TomlFeature right there instead of storing   
  them separately. Then into_toml_feature() becomes self.inline.take().                                                    
                                                                                                                           
  But you're right - the explicit field parsing is unavoidable. The duplication is in the parsing code, not the struct     
  definition. Your current implementation is a valid approach given toml_span's constraints. The maintenance burden is real
   but localized to one place (the deserializer).                                                                          
                                                                                                                           
  If toml_span had #[flatten] like serde, this would be trivial. Without it, explicit parsing is the cost.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Allow to create environments directly without going through features

3 participants