Skip to content

Commit 79a038b

Browse files
committed
rpm: generate subpackage sections in RPM spec template
Add SubPackages() method and supporting types to produce %package, %description, %files, and scriptlet sections for each supplemental package. Refactor Install() to include subpackage artifacts in the shared %install section. Includes comprehensive template-level tests. Signed-off-by: Brian Goff <cpuguy83@gmail.com>
1 parent a20af72 commit 79a038b

10 files changed

Lines changed: 1959 additions & 259 deletions

File tree

Lines changed: 351 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,351 @@
1+
package rpm
2+
3+
import (
4+
"strings"
5+
"testing"
6+
7+
"github.com/moby/buildkit/client/llb"
8+
"github.com/project-dalec/dalec"
9+
"github.com/project-dalec/dalec/internal/test"
10+
"gotest.tools/v3/assert"
11+
"gotest.tools/v3/assert/cmp"
12+
)
13+
14+
func TestRPMSSpec_Buildroot_respects_directory_arg(t *testing.T) {
15+
t.Parallel()
16+
17+
t.Run("when the directory specified is left empty", func(t *testing.T) {
18+
t.Parallel()
19+
spec := baseSpec("myapp")
20+
21+
state := RPMSpec(spec, llb.Scratch(), "", "")
22+
23+
t.Run("the build root is put into /", func(t *testing.T) {
24+
t.Parallel()
25+
ops := test.LLBOpsFromState(t.Context(), t, state)
26+
27+
dirPath := findMkdir(t, ops)
28+
assert.Equal(t, dirPath, "/SPECS/myapp")
29+
30+
filePath, _ := findSpecMkfile(t, ops)
31+
assert.Equal(t, filePath, "/SPECS/myapp/myapp.spec")
32+
})
33+
})
34+
35+
t.Run("when the directory specified is non-empty", func(t *testing.T) {
36+
t.Parallel()
37+
spec := baseSpec("myapp")
38+
state := RPMSpec(spec, llb.Scratch(), "", "custom/dir")
39+
40+
t.Run("the build root is put into that directory", func(t *testing.T) {
41+
t.Parallel()
42+
ops := test.LLBOpsFromState(t.Context(), t, state)
43+
44+
dirPath := findMkdir(t, ops)
45+
assert.Equal(t, dirPath, "/custom/dir")
46+
47+
filePath, _ := findSpecMkfile(t, ops)
48+
assert.Equal(t, filePath, "/custom/dir/myapp.spec")
49+
})
50+
})
51+
}
52+
53+
func TestRPMSpec_Subpackages(t *testing.T) {
54+
t.Parallel()
55+
56+
t.Run("when a spec has no subpackages", func(t *testing.T) {
57+
t.Parallel()
58+
spec := baseSpec("foo")
59+
60+
state := RPMSpec(spec, llb.Scratch(), "azlinux3", "")
61+
ops := test.LLBOpsFromState(t.Context(), t, state)
62+
63+
t.Run("then the rpm spec does not have subpackage markers", func(t *testing.T) {
64+
t.Parallel()
65+
_, data := findSpecMkfile(t, ops)
66+
assert.Assert(t, !strings.Contains(data, "%package -n"))
67+
assert.Assert(t, !strings.Contains(data, "%description -n"))
68+
assert.Assert(t, !strings.Contains(data, "%files -n"))
69+
})
70+
})
71+
72+
t.Run("given a spec has subpackages", func(t *testing.T) {
73+
t.Parallel()
74+
75+
t.Run("when the subpackage uses the default naming", func(t *testing.T) {
76+
t.Parallel()
77+
78+
spec := baseSpec("foo")
79+
spec.Targets = map[string]dalec.Target{
80+
"azlinux3": {
81+
Packages: map[string]dalec.SubPackage{
82+
"debug": {
83+
Description: "Debug symbols for foo",
84+
Artifacts: &dalec.Artifacts{
85+
Binaries: map[string]dalec.ArtifactConfig{
86+
"foo-debug": {},
87+
},
88+
},
89+
},
90+
},
91+
},
92+
}
93+
94+
t.Run("the rpm spec has subpackage markers matching the default name", func(t *testing.T) {
95+
t.Parallel()
96+
state := RPMSpec(spec, llb.Scratch(), "azlinux3", "")
97+
ops := test.LLBOpsFromState(t.Context(), t, state)
98+
99+
_, data := findSpecMkfile(t, ops)
100+
101+
assert.Assert(t, cmp.Contains(data, "%package -n foo-debug"))
102+
assert.Assert(t, cmp.Contains(data, "%description -n foo-debug"))
103+
assert.Assert(t, cmp.Contains(data, "Debug symbols for foo"))
104+
assert.Assert(t, cmp.Contains(data, "%files -n foo-debug"))
105+
assert.Assert(t, cmp.Contains(data, "%{_bindir}/foo-debug"))
106+
})
107+
})
108+
109+
t.Run("when the subpackage has a custom name", func(t *testing.T) {
110+
t.Parallel()
111+
spec := baseSpec("foo")
112+
spec.Targets = map[string]dalec.Target{
113+
"azlinux3": {
114+
Packages: map[string]dalec.SubPackage{
115+
"compat": {
116+
Name: "foo-compat-v2",
117+
Description: "Backward compatibility shim",
118+
Artifacts: &dalec.Artifacts{
119+
Binaries: map[string]dalec.ArtifactConfig{
120+
"foo-v2": {},
121+
},
122+
},
123+
},
124+
},
125+
},
126+
}
127+
128+
t.Run("the rpm spec has subpackage markers matching the custom name", func(t *testing.T) {
129+
t.Parallel()
130+
131+
state := RPMSpec(spec, llb.Scratch(), "azlinux3", "")
132+
ops := test.LLBOpsFromState(t.Context(), t, state)
133+
134+
_, data := findSpecMkfile(t, ops)
135+
136+
assert.Assert(t, cmp.Contains(data, "%package -n foo-compat-v2"))
137+
assert.Assert(t, cmp.Contains(data, "%description -n foo-compat-v2"))
138+
assert.Assert(t, cmp.Contains(data, "%files -n foo-compat-v2"))
139+
assert.Assert(t, cmp.Contains(data, "%{_bindir}/foo-v2"))
140+
// Should NOT contain the default derived name
141+
assert.Assert(t, !strings.Contains(data, "%package -n foo-compat\n"), "should use custom name, not default")
142+
})
143+
})
144+
145+
t.Run("when a subpackage declares package relationships", func(t *testing.T) {
146+
t.Parallel()
147+
spec := baseSpec("foo")
148+
spec.Targets = map[string]dalec.Target{
149+
"azlinux3": {
150+
Packages: map[string]dalec.SubPackage{
151+
"devel": {
152+
Description: "Development files for foo",
153+
Dependencies: &dalec.SubPackageDependencies{
154+
Runtime: dalec.PackageDependencyList{
155+
"foo": dalec.PackageConstraints{
156+
Version: []string{"= %{version}-%{release}"},
157+
},
158+
"libfoo-headers": {},
159+
},
160+
Recommends: dalec.PackageDependencyList{
161+
"foo-docs": {},
162+
},
163+
},
164+
Provides: dalec.PackageDependencyList{
165+
"foo-dev": {},
166+
},
167+
Conflicts: dalec.PackageDependencyList{
168+
"foo-devel-old": {
169+
Version: []string{"< 1.0"},
170+
},
171+
},
172+
Replaces: dalec.PackageDependencyList{
173+
"foo-devel-legacy": {},
174+
},
175+
},
176+
},
177+
},
178+
}
179+
180+
t.Run("the rpm spec includes the declared package relationships", func(t *testing.T) {
181+
t.Parallel()
182+
state := RPMSpec(spec, llb.Scratch(), "azlinux3", "")
183+
ops := test.LLBOpsFromState(t.Context(), t, state)
184+
185+
_, data := findSpecMkfile(t, ops)
186+
187+
assert.Assert(t, cmp.Contains(data, "%package -n foo-devel"))
188+
assert.Assert(t, cmp.Contains(data, "Requires: foo == %{version}-%{release}"))
189+
assert.Assert(t, cmp.Contains(data, "Requires: libfoo-headers"))
190+
assert.Assert(t, cmp.Contains(data, "Recommends: foo-docs"))
191+
assert.Assert(t, cmp.Contains(data, "Provides: foo-dev"))
192+
assert.Assert(t, cmp.Contains(data, "Conflicts: foo-devel-old < 1.0"))
193+
assert.Assert(t, cmp.Contains(data, "Obsoletes: foo-devel-legacy"))
194+
})
195+
})
196+
197+
t.Run("when the spec has multiple subpackages", func(t *testing.T) {
198+
t.Parallel()
199+
spec := baseSpec("foo")
200+
spec.Targets = map[string]dalec.Target{
201+
"azlinux3": {
202+
Packages: map[string]dalec.SubPackage{
203+
"debug": {
204+
Description: "Debug package",
205+
Artifacts: &dalec.Artifacts{
206+
Binaries: map[string]dalec.ArtifactConfig{
207+
"foo-debug": {},
208+
},
209+
},
210+
},
211+
"contrib": {
212+
Description: "Contrib package",
213+
Artifacts: &dalec.Artifacts{
214+
Binaries: map[string]dalec.ArtifactConfig{
215+
"foo-contrib": {},
216+
},
217+
},
218+
},
219+
},
220+
},
221+
}
222+
223+
t.Run("the rpm spec orders the subpackage sections by key", func(t *testing.T) {
224+
t.Parallel()
225+
state := RPMSpec(spec, llb.Scratch(), "azlinux3", "")
226+
ops := test.LLBOpsFromState(t.Context(), t, state)
227+
228+
_, data := findSpecMkfile(t, ops)
229+
230+
assert.Assert(t, cmp.Contains(data, "%package -n foo-contrib"))
231+
assert.Assert(t, cmp.Contains(data, "%package -n foo-debug"))
232+
233+
contribIdx := strings.Index(data, "%package -n foo-contrib")
234+
debugIdx := strings.Index(data, "%package -n foo-debug")
235+
assert.Assert(t, contribIdx < debugIdx)
236+
})
237+
})
238+
239+
t.Run("when the root package and a subpackage have artifacts", func(t *testing.T) {
240+
t.Parallel()
241+
spec := baseSpec("foo")
242+
spec.Artifacts = dalec.Artifacts{
243+
Binaries: map[string]dalec.ArtifactConfig{
244+
"foo": {},
245+
},
246+
}
247+
spec.Targets = map[string]dalec.Target{
248+
"azlinux3": {
249+
Packages: map[string]dalec.SubPackage{
250+
"debug": {
251+
Description: "Debug symbols",
252+
Artifacts: &dalec.Artifacts{
253+
Binaries: map[string]dalec.ArtifactConfig{
254+
"foo-debug": {},
255+
},
256+
},
257+
},
258+
},
259+
},
260+
}
261+
262+
t.Run("the install section includes artifacts from both packages", func(t *testing.T) {
263+
t.Parallel()
264+
state := RPMSpec(spec, llb.Scratch(), "azlinux3", "")
265+
ops := test.LLBOpsFromState(t.Context(), t, state)
266+
267+
_, data := findSpecMkfile(t, ops)
268+
269+
assert.Assert(t, cmp.Contains(data, "cp -r foo %{buildroot}/%{_bindir}/foo"))
270+
assert.Assert(t, cmp.Contains(data, "cp -r foo-debug %{buildroot}/%{_bindir}/foo-debug"))
271+
})
272+
})
273+
274+
t.Run("when a target without subpackages is selected", func(t *testing.T) {
275+
t.Parallel()
276+
spec := baseSpec("foo")
277+
spec.Targets = map[string]dalec.Target{
278+
"azlinux3": {
279+
Packages: map[string]dalec.SubPackage{
280+
"debug": {
281+
Description: "Debug",
282+
},
283+
},
284+
},
285+
}
286+
287+
t.Run("the rpm spec does not have subpackage markers", func(t *testing.T) {
288+
t.Parallel()
289+
state := RPMSpec(spec, llb.Scratch(), "jammy", "")
290+
ops := test.LLBOpsFromState(t.Context(), t, state)
291+
292+
_, data := findSpecMkfile(t, ops)
293+
assert.Assert(t, !strings.Contains(data, "%package -n"))
294+
})
295+
})
296+
})
297+
}
298+
299+
// baseSpec returns a minimal valid spec suitable for RPMSpec().
300+
func baseSpec(name string) *dalec.Spec {
301+
return &dalec.Spec{
302+
Name: name,
303+
Version: "1.0.0",
304+
Revision: "1",
305+
Description: "Test package",
306+
License: "MIT",
307+
}
308+
}
309+
310+
// findSpecMkfile iterates over all LLB ops and returns the path and data of the
311+
// first Mkfile action whose path ends in ".spec".
312+
func findSpecMkfile(t *testing.T, ops []test.LLBOp) (path string, data string) {
313+
t.Helper()
314+
for _, op := range ops {
315+
f := op.Op.GetFile()
316+
if f == nil {
317+
continue
318+
}
319+
for _, a := range f.Actions {
320+
mkfile := a.GetMkfile()
321+
if mkfile == nil {
322+
continue
323+
}
324+
if strings.HasSuffix(mkfile.Path, ".spec") {
325+
return mkfile.Path, string(mkfile.Data)
326+
}
327+
}
328+
}
329+
t.Fatal("no Mkfile action with .spec path found in LLB ops")
330+
return "", ""
331+
}
332+
333+
// findMkdir iterates over all LLB ops and returns the path of the first Mkdir action.
334+
func findMkdir(t *testing.T, ops []test.LLBOp) string {
335+
t.Helper()
336+
for _, op := range ops {
337+
f := op.Op.GetFile()
338+
if f == nil {
339+
continue
340+
}
341+
for _, a := range f.Actions {
342+
mkdir := a.GetMkdir()
343+
if mkdir == nil {
344+
continue
345+
}
346+
return mkdir.Path
347+
}
348+
}
349+
t.Fatal("no Mkdir action found in LLB ops")
350+
return ""
351+
}

0 commit comments

Comments
 (0)