Skip to content

Commit 2b16ba5

Browse files
committed
feat: group deps by provider
dependencies are currently installed and listed in an unassociated way with the provider they come from. From a user POV, associating deps with the specific provider introducing them is crucial in order to eliminate unwanted dependences if using a larger distro like starter. For example, If I am using starter on a daily basis but then go `llama stack build --distro starter...` and get 15 NEW dependencies, there is currently NO way for me to tell which provider introduced them. This work fixes the output of the build process and `--print-deps-only` Signed-off-by: Charlie Doern <[email protected]>
1 parent e195ee3 commit 2b16ba5

File tree

4 files changed

+205
-79
lines changed

4 files changed

+205
-79
lines changed

llama_stack/cli/stack/_build.py

Lines changed: 25 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -230,12 +230,31 @@ def run_stack_build_command(args: argparse.Namespace) -> None:
230230
if args.print_deps_only:
231231
print(f"# Dependencies for {distro_name or args.config or image_name}")
232232
normal_deps, special_deps, external_provider_dependencies = get_provider_dependencies(build_config)
233-
normal_deps += SERVER_DEPENDENCIES
234-
print(f"uv pip install {' '.join(normal_deps)}")
235-
for special_dep in special_deps:
236-
print(f"uv pip install {special_dep}")
237-
for external_dep in external_provider_dependencies:
238-
print(f"uv pip install {external_dep}")
233+
normal_deps["default"] += SERVER_DEPENDENCIES
234+
cprint(
235+
"Please install needed dependencies using the following commands:",
236+
color="yellow",
237+
file=sys.stderr,
238+
)
239+
240+
for prov, deps in normal_deps.items():
241+
if len(deps) == 0:
242+
continue
243+
cprint(f"# Normal Dependencies for {prov}", color="yellow")
244+
cprint(f"uv pip install {' '.join(deps)}", file=sys.stderr)
245+
246+
for prov, deps in special_deps.items():
247+
if len(deps) == 0:
248+
continue
249+
cprint(f"# Special Dependencies for {prov}", color="yellow")
250+
cprint(f"uv pip install {' '.join(deps)}", file=sys.stderr)
251+
252+
for prov, deps in external_provider_dependencies.items():
253+
if len(deps) == 0:
254+
continue
255+
cprint(f"# External Provider Dependencies for {prov}", color="yellow")
256+
cprint(f"uv pip install {' '.join(deps)}", file=sys.stderr)
257+
print()
239258
return
240259

241260
try:

llama_stack/core/build.py

Lines changed: 74 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
# the root directory of this source tree.
66

77
import importlib.resources
8+
import json
89
import sys
910

1011
from pydantic import BaseModel
@@ -41,16 +42,16 @@ class ApiInput(BaseModel):
4142

4243
def get_provider_dependencies(
4344
config: BuildConfig | DistributionTemplate,
44-
) -> tuple[list[str], list[str], list[str]]:
45+
) -> tuple[dict[str, list[str]], dict[str, list[str]], dict[str, list[str]]]:
4546
"""Get normal and special dependencies from provider configuration."""
4647
if isinstance(config, DistributionTemplate):
4748
config = config.build_config()
4849

4950
providers = config.distribution_spec.providers
5051
additional_pip_packages = config.additional_pip_packages
5152

52-
deps = []
53-
external_provider_deps = []
53+
deps = {}
54+
external_provider_deps = {}
5455
registry = get_provider_registry(config)
5556
for api_str, provider_or_providers in providers.items():
5657
providers_for_api = registry[Api(api_str)]
@@ -69,37 +70,86 @@ def get_provider_dependencies(
6970
# this ensures we install the top level module for our external providers
7071
if provider_spec.module:
7172
if isinstance(provider_spec.module, str):
72-
external_provider_deps.append(provider_spec.module)
73+
external_provider_deps.setdefault(provider_spec.provider_type, []).append(provider_spec.module)
7374
else:
74-
external_provider_deps.extend(provider_spec.module)
75+
external_provider_deps.setdefault(provider_spec.provider_type, []).extend(provider_spec.module)
7576
if hasattr(provider_spec, "pip_packages"):
76-
deps.extend(provider_spec.pip_packages)
77+
deps.setdefault(provider_spec.provider_type, []).extend(provider_spec.pip_packages)
7778
if hasattr(provider_spec, "container_image") and provider_spec.container_image:
7879
raise ValueError("A stack's dependencies cannot have a container image")
7980

80-
normal_deps = []
81-
special_deps = []
82-
for package in deps:
83-
if "--no-deps" in package or "--index-url" in package:
84-
special_deps.append(package)
81+
normal_deps = {}
82+
special_deps = {}
83+
for provider, package in deps.items():
84+
if any("--no-deps" in s for s in package) or any("--index-url" in s for s in package):
85+
special_deps.setdefault(provider, []).append(package)
8586
else:
86-
normal_deps.append(package)
87+
normal_deps.setdefault(provider, []).append(package)
8788

88-
normal_deps.extend(additional_pip_packages or [])
89+
normal_deps["default"] = additional_pip_packages or []
8990

90-
return list(set(normal_deps)), list(set(special_deps)), list(set(external_provider_deps))
91+
# Helper function to flatten and deduplicate dependencies
92+
def flatten_and_dedup(deps_list):
93+
flattened = []
94+
for item in deps_list:
95+
if isinstance(item, list):
96+
flattened.extend(item)
97+
else:
98+
flattened.append(item)
99+
return list(set(flattened))
100+
101+
for key in normal_deps.keys():
102+
normal_deps[key] = flatten_and_dedup(normal_deps[key])
103+
for key in special_deps.keys():
104+
special_deps[key] = flatten_and_dedup(special_deps[key])
105+
for key in external_provider_deps.keys():
106+
external_provider_deps[key] = flatten_and_dedup(external_provider_deps[key])
107+
108+
return normal_deps, special_deps, external_provider_deps
91109

92110

93111
def print_pip_install_help(config: BuildConfig):
94-
normal_deps, special_deps, _ = get_provider_dependencies(config)
112+
normal_deps, special_deps, external_provider_dependencies = get_provider_dependencies(config)
113+
normal_deps["default"] += SERVER_DEPENDENCIES
114+
cprint(
115+
"Please install needed dependencies using the following commands:",
116+
color="yellow",
117+
file=sys.stderr,
118+
)
119+
120+
for provider, deps in normal_deps.items():
121+
if len(deps) == 0:
122+
continue
123+
cprint(f"# Normal Dependencies for {provider}", color="yellow")
124+
cprint(f"uv pip install {' '.join(deps)}", file=sys.stderr)
125+
126+
for provider, deps in special_deps.items():
127+
if len(deps) == 0:
128+
continue
129+
cprint(f"# Special Dependencies for {provider}", color="yellow")
130+
cprint(f"uv pip install {' '.join(deps)}", file=sys.stderr)
131+
132+
for provider, deps in external_provider_dependencies.items():
133+
if len(deps) == 0:
134+
continue
135+
cprint(f"# External Provider Dependencies for {provider}", color="yellow")
136+
cprint(f"uv pip install {' '.join(deps)}", file=sys.stderr)
137+
print()
138+
return
95139

96140
cprint(
97-
f"Please install needed dependencies using the following commands:\n\nuv pip install {' '.join(normal_deps)}",
141+
"Please install needed dependencies using the following commands:",
98142
color="yellow",
99143
file=sys.stderr,
100144
)
101-
for special_dep in special_deps:
102-
cprint(f"uv pip install {special_dep}", color="yellow", file=sys.stderr)
145+
146+
for provider, deps in normal_deps.items():
147+
cprint(f"# Normal Dependencies for {provider}")
148+
cprint(f"uv pip install {deps}", color="yellow", file=sys.stderr)
149+
150+
for provider, deps in special_deps.items():
151+
cprint(f"# Special Dependencies for {provider}")
152+
cprint(f"uv pip install {deps}", color="yellow", file=sys.stderr)
103153
print()
104154

105155

@@ -112,12 +162,12 @@ def build_image(
112162
container_base = build_config.distribution_spec.container_image or "python:3.12-slim"
113163

114164
normal_deps, special_deps, external_provider_deps = get_provider_dependencies(build_config)
115-
normal_deps += SERVER_DEPENDENCIES
165+
normal_deps["default"] += SERVER_DEPENDENCIES
116166
if build_config.external_apis_dir:
117167
external_apis = load_external_apis(build_config)
118168
if external_apis:
119169
for _, api_spec in external_apis.items():
120-
normal_deps.extend(api_spec.pip_packages)
170+
normal_deps["default"].extend(api_spec.pip_packages)
121171

122172
if build_config.image_type == LlamaStackImageType.CONTAINER.value:
123173
script = str(importlib.resources.files("llama_stack") / "core/build_container.sh")
@@ -130,7 +180,7 @@ def build_image(
130180
"--container-base",
131181
container_base,
132182
"--normal-deps",
133-
" ".join(normal_deps),
183+
json.dumps(normal_deps),
134184
]
135185
# When building from a config file (not a template), include the run config path in the
136186
# build arguments
@@ -143,15 +193,15 @@ def build_image(
143193
"--env-name",
144194
str(image_name),
145195
"--normal-deps",
146-
" ".join(normal_deps),
196+
json.dumps(normal_deps),
147197
]
148198

149199
# Always pass both arguments, even if empty, to maintain consistent positional arguments
150200
if special_deps:
151-
args.extend(["--optional-deps", "#".join(special_deps)])
201+
args.extend(["--optional-deps", json.dumps(special_deps)])
152202
if external_provider_deps:
153203
args.extend(
154-
["--external-provider-deps", "#".join(external_provider_deps)]
204+
["--external-provider-deps", json.dumps(external_provider_deps)]
155205
) # the script will install external provider module, get its deps, and install those too.
156206

157207
return_code = run_command(args)

llama_stack/core/build_container.sh

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ NC='\033[0m' # No Color
3232
# Usage function
3333
usage() {
3434
echo "Usage: $0 --image-name <image_name> --container-base <container_base> --normal-deps <pip_dependencies> [--run-config <run_config>] [--external-provider-deps <external_provider_deps>] [--optional-deps <special_pip_deps>]"
35-
echo "Example: $0 --image-name llama-stack-img --container-base python:3.12-slim --normal-deps 'numpy pandas' --run-config ./run.yaml --external-provider-deps 'foo' --optional-deps 'bar'"
35+
echo "Example: $0 --image-name llama-stack-img --container-base python:3.12-slim --normal-deps '{\"default\": [\"numpy\", \"pandas\"]}' --run-config ./run.yaml --external-provider-deps '{\"vector_db\": [\"chromadb\"]}' --optional-deps '{\"special\": [\"bar\"]}'"
3636
exit 1
3737
}
3838

@@ -74,7 +74,7 @@ while [[ $# -gt 0 ]]; do
7474
;;
7575
--external-provider-deps)
7676
if [[ -z "$2" || "$2" == --* ]]; then
77-
echo "Error: --external-provider-deps requires a string value" >&2
77+
echo "Error: --external-provider-deps requires a JSON object" >&2
7878
usage
7979
fi
8080
external_provider_deps="$2"
@@ -182,26 +182,17 @@ RUN uv pip install --no-cache $quoted_deps
182182
EOF
183183
fi
184184

185-
if [ -n "$optional_deps" ]; then
186-
IFS='#' read -ra parts <<<"$optional_deps"
187-
for part in "${parts[@]}"; do
188-
read -ra pip_args <<< "$part"
189-
quoted_deps=$(printf " %q" "${pip_args[@]}")
190-
add_to_container <<EOF
191-
RUN uv pip install --no-cache $quoted_deps
192-
EOF
193-
done
194-
fi
195-
196185
if [ -n "$external_provider_deps" ]; then
197-
IFS='#' read -ra parts <<<"$external_provider_deps"
198-
for part in "${parts[@]}"; do
199-
read -ra pip_args <<< "$part"
200-
quoted_deps=$(printf " %q" "${pip_args[@]}")
201-
add_to_container <<EOF
202-
RUN uv pip install --no-cache $quoted_deps
186+
echo "Installing external provider dependencies from JSON: $external_provider_deps"
187+
# Parse JSON and iterate packages only (no flags or URLs assumed)
188+
echo "$external_provider_deps" | jq -r 'to_entries[] | .value[]' | while read -r part; do
189+
if [ -n "$part" ]; then
190+
echo "Installing external provider module: $part"
191+
add_to_container <<EOF
192+
RUN uv pip install --no-cache $part
203193
EOF
204-
add_to_container <<EOF
194+
echo "Getting provider spec for module: $part"
195+
add_to_container <<EOF
205196
RUN python3 - <<PYTHON | uv pip install --no-cache -r -
206197
import importlib
207198
import sys
@@ -217,6 +208,28 @@ except Exception as e:
217208
print(f'Error getting provider spec for {package_name}: {e}', file=sys.stderr)
218209
PYTHON
219210
EOF
211+
fi
212+
done
213+
fi
214+
215+
if [ -n "$optional_deps" ]; then
216+
echo "Installing optional dependencies from JSON: $optional_deps"
217+
# For optional deps, process each spec separately to preserve flags like --no-deps
218+
last_provider=""
219+
echo "$optional_deps" | jq -r 'to_entries[] | .key as $k | .value[] | "\($k)\t\(.)"' | while IFS=$'\t' read -r provider spec; do
220+
if [ -n "$spec" ]; then
221+
if [ "$provider" != "$last_provider" ]; then
222+
echo "Installing optional dependencies for provider '$provider'"
223+
last_provider="$provider"
224+
fi
225+
echo "Installing dependency: $spec"
226+
# Split spec into args and install
227+
read -r -a pip_args <<< "$spec"
228+
quoted_deps=$(printf " %q" "${pip_args[@]}")
229+
add_to_container <<EOF
230+
RUN uv pip install --no-cache $quoted_deps
231+
EOF
232+
fi
220233
done
221234
fi
222235

0 commit comments

Comments
 (0)