Skip to content

Commit ce0050c

Browse files
edmundmillerclaude
andcommitted
feat: Add core package management abstraction
Creates the foundation for unified package management with: - PackageSpec: Unified specification for packages across providers - PackageProvider: Interface for package manager implementations - PackageManager: Central coordinator for package providers - PackageProviderExtension: Plugin extension point for providers This abstraction layer enables consistent package management across different tools (conda, pixi, mamba, etc.) while supporting both simple and advanced configuration patterns. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]> Signed-off-by: Edmund Miller <[email protected]>
1 parent d57009a commit ce0050c

File tree

4 files changed

+415
-0
lines changed

4 files changed

+415
-0
lines changed
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
/*
2+
* Copyright 2013-2024, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package nextflow.packages
18+
19+
import java.nio.file.Path
20+
import java.util.concurrent.ConcurrentHashMap
21+
22+
import groovy.transform.CompileStatic
23+
import groovy.util.logging.Slf4j
24+
import nextflow.Session
25+
import nextflow.plugin.PluginsFacade
26+
27+
/**
28+
* Manages package providers and coordinates package environment creation
29+
*
30+
* @author Edmund Miller <[email protected]>
31+
*/
32+
@Slf4j
33+
@CompileStatic
34+
class PackageManager {
35+
36+
private final Map<String, PackageProvider> providers = new ConcurrentHashMap<>()
37+
private final Session session
38+
39+
PackageManager(Session session) {
40+
this.session = session
41+
initializeProviders()
42+
}
43+
44+
/**
45+
* Initialize available package providers from plugins
46+
*/
47+
private void initializeProviders() {
48+
// Load package providers from plugins
49+
def extensions = PluginsFacade.getExtensions(PackageProviderExtension)
50+
for (PackageProviderExtension extension : extensions) {
51+
def provider = extension.createProvider(session)
52+
if (provider && provider.isAvailable()) {
53+
providers.put(provider.getName(), provider)
54+
log.debug "Registered package provider: ${provider.getName()}"
55+
}
56+
}
57+
}
58+
59+
/**
60+
* Get a package provider by name
61+
*
62+
* @param name Provider name (e.g., "conda", "pixi")
63+
* @return The package provider or null if not found
64+
*/
65+
PackageProvider getProvider(String name) {
66+
return providers.get(name)
67+
}
68+
69+
/**
70+
* Get all available package providers
71+
*
72+
* @return Map of provider name to provider instance
73+
*/
74+
Map<String, PackageProvider> getProviders() {
75+
return Collections.unmodifiableMap(providers)
76+
}
77+
78+
/**
79+
* Create a package environment using the appropriate provider
80+
*
81+
* @param spec The package specification
82+
* @return The path to the created environment
83+
*/
84+
Path createEnvironment(PackageSpec spec) {
85+
if (!spec.isValid()) {
86+
throw new IllegalArgumentException("Invalid package specification: ${spec}")
87+
}
88+
89+
def provider = getProvider(spec.provider)
90+
if (!provider) {
91+
throw new IllegalArgumentException("Package provider not found: ${spec.provider}")
92+
}
93+
94+
if (!provider.supportsSpec(spec)) {
95+
throw new IllegalArgumentException("Package specification not supported by provider ${spec.provider}: ${spec}")
96+
}
97+
98+
return provider.createEnvironment(spec)
99+
}
100+
101+
/**
102+
* Get the activation script for an environment
103+
*
104+
* @param spec The package specification
105+
* @param envPath Path to the environment
106+
* @return Shell script snippet to activate the environment
107+
*/
108+
String getActivationScript(PackageSpec spec, Path envPath) {
109+
def provider = getProvider(spec.provider)
110+
if (!provider) {
111+
throw new IllegalArgumentException("Package provider not found: ${spec.provider}")
112+
}
113+
114+
return provider.getActivationScript(envPath)
115+
}
116+
117+
/**
118+
* Parse a package specification from process configuration
119+
*
120+
* @param packageDef Package definition (string or map)
121+
* @param provider Default provider if not specified
122+
* @return Parsed package specification
123+
*/
124+
static PackageSpec parseSpec(Object packageDef, String provider = null) {
125+
if (packageDef instanceof String) {
126+
return new PackageSpec(provider, [packageDef])
127+
} else if (packageDef instanceof List) {
128+
return new PackageSpec(provider, packageDef as List<String>)
129+
} else if (packageDef instanceof Map) {
130+
def map = packageDef as Map
131+
def spec = new PackageSpec()
132+
133+
if (map.containsKey('provider')) {
134+
spec.provider = map.provider as String
135+
} else if (provider) {
136+
spec.provider = provider
137+
}
138+
139+
if (map.containsKey('packages')) {
140+
def packages = map.packages
141+
if (packages instanceof String) {
142+
spec.entries = [packages]
143+
} else if (packages instanceof List) {
144+
spec.entries = packages as List<String>
145+
}
146+
}
147+
148+
if (map.containsKey('environment')) {
149+
spec.environment = map.environment as String
150+
}
151+
152+
if (map.containsKey('channels')) {
153+
def channels = map.channels
154+
if (channels instanceof List) {
155+
spec.channels = channels as List<String>
156+
} else if (channels instanceof String) {
157+
spec.channels = [channels]
158+
}
159+
}
160+
161+
if (map.containsKey('options')) {
162+
spec.options = map.options as Map<String, Object>
163+
}
164+
165+
return spec
166+
}
167+
168+
throw new IllegalArgumentException("Invalid package definition: ${packageDef}")
169+
}
170+
171+
/**
172+
* Check if the package manager feature is enabled
173+
*
174+
* @param session The current session
175+
* @return True if the feature is enabled
176+
*/
177+
static boolean isEnabled(Session session) {
178+
return session.config.navigate('nextflow.preview.package', false) as Boolean
179+
}
180+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
/*
2+
* Copyright 2013-2024, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package nextflow.packages
18+
19+
import java.nio.file.Path
20+
21+
import groovy.transform.CompileStatic
22+
23+
/**
24+
* Interface for package providers (conda, pixi, mamba, etc.)
25+
*
26+
* @author Edmund Miller <[email protected]>
27+
*/
28+
@CompileStatic
29+
interface PackageProvider {
30+
31+
/**
32+
* @return The name of this package provider (e.g., "conda", "pixi")
33+
*/
34+
String getName()
35+
36+
/**
37+
* @return Whether this package provider is available on the system
38+
*/
39+
boolean isAvailable()
40+
41+
/**
42+
* Create or get a cached environment for the given package specification
43+
*
44+
* @param spec The package specification
45+
* @return The path to the environment
46+
*/
47+
Path createEnvironment(PackageSpec spec)
48+
49+
/**
50+
* Get the shell activation script for the given environment
51+
*
52+
* @param envPath Path to the environment
53+
* @return Shell script snippet to activate the environment
54+
*/
55+
String getActivationScript(Path envPath)
56+
57+
/**
58+
* Check if the given package specification is valid for this provider
59+
*
60+
* @param spec The package specification
61+
* @return True if the spec is valid for this provider
62+
*/
63+
boolean supportsSpec(PackageSpec spec)
64+
65+
/**
66+
* Get provider-specific configuration
67+
*
68+
* @return Configuration object for this provider
69+
*/
70+
Object getConfig()
71+
}
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
* Copyright 2013-2024, Seqera Labs
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package nextflow.packages
18+
19+
import groovy.transform.CompileStatic
20+
import nextflow.Session
21+
import org.pf4j.ExtensionPoint
22+
23+
/**
24+
* Extension point for package provider plugins
25+
*
26+
* @author Edmund Miller <[email protected]>
27+
*/
28+
@CompileStatic
29+
interface PackageProviderExtension extends ExtensionPoint {
30+
31+
/**
32+
* Create a package provider instance
33+
*
34+
* @param session The current Nextflow session
35+
* @return A package provider instance
36+
*/
37+
PackageProvider createProvider(Session session)
38+
39+
/**
40+
* Get the priority of this extension (higher values take precedence)
41+
*
42+
* @return Priority value
43+
*/
44+
int getPriority()
45+
}

0 commit comments

Comments
 (0)