Skip to content

Commit d9d08f6

Browse files
committed
feat(sbom): add support for rust-cargo package type
Signed-off-by: Radmir Khurum <radmir.khurum@flant.com>
1 parent eae7dfb commit d9d08f6

21 files changed

Lines changed: 622 additions & 27 deletions

File tree

.specify/extensions.yml

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,14 @@
11
installed:
2-
- brownfield
2+
- brownfield
33
settings:
44
auto_execute_hooks: true
55
hooks:
66
after_init:
7-
- extension: brownfield
8-
command: speckit.brownfield.scan
9-
enabled: true
10-
optional: true
11-
priority: 10
12-
prompt: Scan this existing project to customize spec-kit configuration?
13-
description: Auto-scan project after spec-kit init to detect tech stack and conventions
14-
condition: null
7+
- extension: brownfield
8+
command: speckit.brownfield.scan
9+
enabled: true
10+
optional: true
11+
priority: 10
12+
prompt: Scan this existing project to customize spec-kit configuration?
13+
description: Auto-scan project after spec-kit init to detect tech stack and conventions
14+
condition: null

docs/_data/werf_yaml.yml

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -588,23 +588,23 @@ sections:
588588
- name: type
589589
value: "string"
590590
description:
591-
en: "Package ecosystem type (go-mod, python-uv, python-pip, python-poetry; aliases: uv, pip, poetry)"
592-
ru: "Тип экосистемы пакетов (go-mod, python-uv, python-pip, python-poetry; алиасы: uv, pip, poetry)"
591+
en: "Package ecosystem type (go-mod, python-uv, python-pip, python-poetry, rust-cargo; aliases: uv, pip, poetry, cargo)"
592+
ru: "Тип экосистемы пакетов (go-mod, python-uv, python-pip, python-poetry, rust-cargo; алиасы: uv, pip, poetry, cargo)"
593593
- name: workdir
594594
value: "string"
595595
description:
596-
en: "Directory inside the image that contains the manifest files (required for go-mod and python-*)"
597-
ru: "Директория внутри образа, содержащая файлы манифеста (обязательно для go-mod и python-*)"
596+
en: "Directory inside the image that contains the manifest files (required for go-mod, python-*, and rust-cargo)"
597+
ru: "Директория внутри образа, содержащая файлы манифеста (обязательно для go-mod, python-* и rust-cargo)"
598598
- name: spec
599599
value: "string"
600600
description:
601-
en: "Manifest file name (defaults: go.mod for go-mod, pyproject.toml for python-uv/python-poetry, requirements.txt for python-pip)"
602-
ru: "Имя файла манифеста (по умолчанию: go.mod для go-mod, pyproject.toml для python-uv/python-poetry, requirements.txt для python-pip)"
601+
en: "Manifest file name (defaults: go.mod for go-mod, pyproject.toml for python-uv/python-poetry, requirements.txt for python-pip, Cargo.toml for rust-cargo)"
602+
ru: "Имя файла манифеста (по умолчанию: go.mod для go-mod, pyproject.toml для python-uv/python-poetry, requirements.txt для python-pip, Cargo.toml для rust-cargo)"
603603
- name: lock
604604
value: "string"
605605
description:
606-
en: "Lock file name (defaults: go.sum for go-mod, uv.lock for python-uv, poetry.lock for python-poetry; empty for python-pip)"
607-
ru: "Имя lock-файла (по умолчанию: go.sum для go-mod, uv.lock для python-uv, poetry.lock для python-poetry; пусто для python-pip)"
606+
en: "Lock file name (defaults: go.sum for go-mod, uv.lock for python-uv, poetry.lock for python-poetry, Cargo.lock for rust-cargo; empty for python-pip)"
607+
ru: "Имя lock-файла (по умолчанию: go.sum для go-mod, uv.lock для python-uv, poetry.lock для python-poetry, Cargo.lock для rust-cargo; пусто для python-pip)"
608608
- <<: *meta-section-build-cache-version
609609
- name: platform
610610
description:

docs/pages_en/usage/build/stapel/instructions.md

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,7 +140,13 @@ No limitations are imposed on assembly instructions. The suggested use of _user
140140

141141
## Installing binary packages
142142

143-
The `packages` directive provides a declarative way to install binary packages with the `pm` package manager. werf installs the listed packages in a dedicated `packagesInstall` stage that runs before the `install` stage. When SBOM generation is enabled (`build.sbom.enable: true`), the installed packages — including their transitive dependencies — are recorded in the resulting image SBOM.
143+
The `packages` directive provides a declarative way to declare package dependencies. werf processes each entry in a dedicated `packagesInstall` stage that runs before the `install` stage. When SBOM generation is enabled (`build.sbom.enable: true`), the installed packages — including their transitive dependencies — are recorded in the resulting image SBOM.
144+
145+
Two kinds of package sources are supported: OS package manager (`os-pm`) and file-based package ecosystems (`go-mod`, `python-uv`, `python-pip`, `python-poetry`, `rust-cargo`).
146+
147+
### OS packages
148+
149+
The `os-pm` type installs system packages via the `pm` package manager bundled in the builder base image:
144150

145151
```yaml
146152
image: app
@@ -152,15 +158,80 @@ packages:
152158
- jq
153159
```
154160

155-
The directive supports the following options:
156-
157-
- `type` — the package source type. Only `os-pm` (the `pm` package manager) is currently supported.
158-
- `spec` — an inline list of package names to install. File-based package lists are not supported.
161+
- `type: os-pm` — selects the `pm` package manager.
162+
- `spec` — an inline list of package names to install.
159163

160164
The base image must provide the `pm` binary in `$PATH` (for example, a Deckhouse builder base image labeled `io.deckhouse.internal.builder=true`). Otherwise the build fails because `pm` cannot be found.
161165

162166
Packages installed in a parent image are inherited by images based on it via `fromImage` and remain present in the child image SBOM.
163167

168+
### File-based package ecosystems
169+
170+
File-based types run the ecosystem's install command inside the build container and feed the resulting lock file to syft for SBOM generation. The package manager itself must be pre-installed in the builder image.
171+
172+
**Go modules** (`go-mod`):
173+
174+
```yaml
175+
packages:
176+
- type: go-mod
177+
workdir: /app
178+
```
179+
180+
Runs `go mod download`. Default files: `go.mod` (spec) and `go.sum` (lock).
181+
182+
**Python — uv** (`python-uv`, alias `uv`):
183+
184+
```yaml
185+
packages:
186+
- type: python-uv
187+
workdir: /app
188+
```
189+
190+
Runs `uv sync --frozen`. Default files: `pyproject.toml` (spec) and `uv.lock` (lock).
191+
192+
**Python — pip** (`python-pip`, alias `pip`):
193+
194+
```yaml
195+
packages:
196+
- type: python-pip
197+
workdir: /app
198+
```
199+
200+
Runs `pip install --no-cache-dir -r requirements.txt`. Default spec: `requirements.txt`. No lock file (pip has no lock semantics; the `lock` field is rejected).
201+
202+
**Python — poetry** (`python-poetry`, alias `poetry`):
203+
204+
```yaml
205+
packages:
206+
- type: python-poetry
207+
workdir: /app
208+
```
209+
210+
Runs `poetry install --no-root`. Default files: `pyproject.toml` (spec) and `poetry.lock` (lock).
211+
212+
**Rust — Cargo** (`rust-cargo`, alias `cargo`):
213+
214+
```yaml
215+
packages:
216+
- type: rust-cargo
217+
workdir: /app
218+
```
219+
220+
Runs `cargo fetch`. Default files: `Cargo.toml` (spec) and `Cargo.lock` (lock). syft uses the `rust-cargo-lock-cataloger` to scan the lock file.
221+
222+
All file-based types support `workdir` (required), `spec` (optional, overrides default manifest filename), and `lock` (optional, overrides default lock filename). Multiple entries of the same or different types can be combined in one image:
223+
224+
```yaml
225+
packages:
226+
- type: go-mod
227+
workdir: /app
228+
- type: rust-cargo
229+
workdir: /app/native
230+
- type: os-pm
231+
spec:
232+
- libssl-dev
233+
```
234+
164235
## Syntax
165236

166237
There are two mutually exclusive top-level ***builder directives*** for assembly instructions: `shell` and `ansible`. You can build an image either via ***shell instructions*** or via their ***ansible counterparts***.

docs/pages_ru/usage/build/stapel/instructions.md

Lines changed: 76 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,13 @@ shell:
153153

154154
## Установка бинарных пакетов
155155

156-
Директива `packages` позволяет декларативно устанавливать бинарные пакеты с помощью пакетного менеджера `pm`. werf устанавливает перечисленные пакеты в отдельной стадии `packagesInstall`, которая выполняется до стадии `install`. Если включена генерация SBOM (`build.sbom.enable: true`), установленные пакеты — вместе с их транзитивными зависимостями — попадают в итоговый SBOM образа.
156+
Директива `packages` позволяет декларативно объявлять зависимости от пакетов. werf обрабатывает каждую запись в отдельной стадии `packagesInstall`, которая выполняется до стадии `install`. Если включена генерация SBOM (`build.sbom.enable: true`), установленные пакеты — вместе с их транзитивными зависимостями — попадают в итоговый SBOM образа.
157+
158+
Поддерживаются два вида источников пакетов: менеджер системных пакетов (`os-pm`) и файловые экосистемы (`go-mod`, `python-uv`, `python-pip`, `python-poetry`, `rust-cargo`).
159+
160+
### Системные пакеты
161+
162+
Тип `os-pm` устанавливает системные пакеты через пакетный менеджер `pm`, входящий в состав базового образа сборщика:
157163

158164
```yaml
159165
image: app
@@ -165,15 +171,80 @@ packages:
165171
- jq
166172
```
167173

168-
Директива поддерживает следующие опции:
169-
170-
- `type` — тип источника пакетов. Сейчас поддерживается только `os-pm` (пакетный менеджер `pm`).
171-
- `spec` — inline-список имён пакетов для установки. Списки пакетов из файла не поддерживаются.
174+
- `type: os-pm` — выбирает пакетный менеджер `pm`.
175+
- `spec` — inline-список имён пакетов для установки.
172176

173177
Базовый образ должен предоставлять бинарь `pm` в `$PATH` (например, базовый образ Deckhouse с меткой `io.deckhouse.internal.builder=true`). Иначе сборка завершится ошибкой, так как `pm` не будет найден.
174178

175179
Пакеты, установленные в родительском образе, наследуются образами, построенными на его основе через `fromImage`, и остаются в SBOM дочернего образа.
176180

181+
### Файловые экосистемы пакетов
182+
183+
Файловые типы запускают команду установки экосистемы внутри контейнера сборки и передают полученный lock-файл в syft для генерации SBOM. Пакетный менеджер должен быть предустановлен в образе сборщика.
184+
185+
**Go-модули** (`go-mod`):
186+
187+
```yaml
188+
packages:
189+
- type: go-mod
190+
workdir: /app
191+
```
192+
193+
Выполняет `go mod download`. Файлы по умолчанию: `go.mod` (spec) и `go.sum` (lock).
194+
195+
**Python — uv** (`python-uv`, алиас `uv`):
196+
197+
```yaml
198+
packages:
199+
- type: python-uv
200+
workdir: /app
201+
```
202+
203+
Выполняет `uv sync --frozen`. Файлы по умолчанию: `pyproject.toml` (spec) и `uv.lock` (lock).
204+
205+
**Python — pip** (`python-pip`, алиас `pip`):
206+
207+
```yaml
208+
packages:
209+
- type: python-pip
210+
workdir: /app
211+
```
212+
213+
Выполняет `pip install --no-cache-dir -r requirements.txt`. Spec по умолчанию: `requirements.txt`. Lock-файл не поддерживается (pip не имеет семантики lock; поле `lock` отклоняется с ошибкой).
214+
215+
**Python — poetry** (`python-poetry`, алиас `poetry`):
216+
217+
```yaml
218+
packages:
219+
- type: python-poetry
220+
workdir: /app
221+
```
222+
223+
Выполняет `poetry install --no-root`. Файлы по умолчанию: `pyproject.toml` (spec) и `poetry.lock` (lock).
224+
225+
**Rust — Cargo** (`rust-cargo`, алиас `cargo`):
226+
227+
```yaml
228+
packages:
229+
- type: rust-cargo
230+
workdir: /app
231+
```
232+
233+
Выполняет `cargo fetch`. Файлы по умолчанию: `Cargo.toml` (spec) и `Cargo.lock` (lock). Для генерации SBOM syft использует каталогизатор `rust-cargo-lock-cataloger`.
234+
235+
Все файловые типы поддерживают поля `workdir` (обязательно), `spec` (опционально, переопределяет имя файла манифеста) и `lock` (опционально, переопределяет имя lock-файла). В одном образе можно комбинировать несколько записей одного или разных типов:
236+
237+
```yaml
238+
packages:
239+
- type: go-mod
240+
workdir: /app
241+
- type: rust-cargo
242+
workdir: /app/native
243+
- type: os-pm
244+
spec:
245+
- libssl-dev
246+
```
247+
177248
## Синтаксис
178249

179250
Пользовательские стадии и инструкции сборки определяются внутри двух взаимоисключающих директив — `shell` и `ansible`. Каждый образ может собираться либо используя сборочные инструкции ***shell***, либо задачи ***ansible***, описанные в соответствующих директивах.

pkg/build/stage/packages_test.go

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -222,6 +222,29 @@ var _ = Describe("GeneratePackagesCommands", func() {
222222
},
223223
{Type: config.PackagesDirectiveTypeOSPM, Spec: config.PackagesSpec{Packages: []string{"curl"}}},
224224
}, []string{"cd \"/app\" && go mod download", "cd \"/lib\" && uv sync --frozen", config.ContainerFactoryVersionSnapshotCmd(), "pm install curl"}),
225+
226+
Entry("rust-cargo /app produces cargo fetch", []*config.PackagesDirective{
227+
{Type: config.PackagesDirectiveTypeRustCargo, FileBased: config.FileBasedSpec{Workdir: "/app"}},
228+
}, []string{"cd \"/app\" && cargo fetch"}),
229+
230+
Entry("rust-cargo /src/service produces cargo fetch", []*config.PackagesDirective{
231+
{Type: config.PackagesDirectiveTypeRustCargo, FileBased: config.FileBasedSpec{Workdir: "/src/service"}},
232+
}, []string{"cd \"/src/service\" && cargo fetch"}),
233+
234+
Entry("rust-cargo workdir with spaces is quoted", []*config.PackagesDirective{
235+
{Type: config.PackagesDirectiveTypeRustCargo, FileBased: config.FileBasedSpec{Workdir: "/my app/crate"}},
236+
}, []string{"cd \"/my app/crate\" && cargo fetch"}),
237+
238+
Entry("multiple rust-cargo entries", []*config.PackagesDirective{
239+
{Type: config.PackagesDirectiveTypeRustCargo, FileBased: config.FileBasedSpec{Workdir: "/app"}},
240+
{Type: config.PackagesDirectiveTypeRustCargo, FileBased: config.FileBasedSpec{Workdir: "/lib"}},
241+
}, []string{"cd \"/app\" && cargo fetch", "cd \"/lib\" && cargo fetch"}),
242+
243+
Entry("mixed: rust-cargo + go-mod + os-pm all produce commands", []*config.PackagesDirective{
244+
{Type: config.PackagesDirectiveTypeRustCargo, FileBased: config.FileBasedSpec{Workdir: "/app"}},
245+
{Type: config.PackagesDirectiveTypeGoMod, FileBased: config.FileBasedSpec{Workdir: "/tools"}},
246+
{Type: config.PackagesDirectiveTypeOSPM, Spec: config.PackagesSpec{Packages: []string{"libssl-dev"}}},
247+
}, []string{"cd \"/app\" && cargo fetch", "cd \"/tools\" && go mod download", config.ContainerFactoryVersionSnapshotCmd(), "pm install libssl-dev"}),
225248
)
226249
})
227250

pkg/config/packages_directive.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ const (
1414
PackagesDirectiveTypePythonUV PackagesDirectiveType = "python-uv"
1515
PackagesDirectiveTypePythonPip PackagesDirectiveType = "python-pip"
1616
PackagesDirectiveTypePythonPoetry PackagesDirectiveType = "python-poetry"
17+
PackagesDirectiveTypeRustCargo PackagesDirectiveType = "rust-cargo"
1718
)
1819

1920
type PackagesSpec struct {
@@ -69,6 +70,14 @@ var ecosystems = map[PackagesDirectiveType]PackageEcosystem{
6970
InstallCmd: func(workdir, _ string) string { return fmt.Sprintf("cd %q && poetry install --no-root", workdir) },
7071
CatalogerName: "python-package-cataloger",
7172
},
73+
PackagesDirectiveTypeRustCargo: {
74+
Type: PackagesDirectiveTypeRustCargo,
75+
Aliases: []string{"cargo"},
76+
DefaultSpec: "Cargo.toml",
77+
DefaultLock: "Cargo.lock",
78+
InstallCmd: func(workdir, _ string) string { return fmt.Sprintf("cd %q && cargo fetch", workdir) },
79+
CatalogerName: "rust-cargo-lock-cataloger",
80+
},
7281
}
7382

7483
var aliasToType = func() map[string]PackagesDirectiveType {

0 commit comments

Comments
 (0)