Skip to content

Commit a836231

Browse files
committed
add parsing of flavors
1 parent ed490bb commit a836231

File tree

2 files changed

+273
-0
lines changed

2 files changed

+273
-0
lines changed

src/python_gardenlinux_lib/flavors/__init__.py

Whitespace-only changes.
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
#!/usr/bin/env python
2+
import yaml
3+
import sys
4+
import subprocess
5+
import os
6+
import argparse
7+
import fnmatch
8+
import json
9+
from jsonschema import validate, ValidationError
10+
11+
12+
# Define the schema for validation
13+
SCHEMA = {
14+
"type": "object",
15+
"properties": {
16+
"targets": {
17+
"type": "array",
18+
"items": {
19+
"type": "object",
20+
"properties": {
21+
"name": {"type": "string"},
22+
"category": {"type": "string"},
23+
"flavors": {
24+
"type": "array",
25+
"items": {
26+
"type": "object",
27+
"properties": {
28+
"features": {
29+
"type": "array",
30+
"items": {"type": "string"},
31+
},
32+
"arch": {"type": "string"},
33+
"build": {"type": "boolean"},
34+
"test": {"type": "boolean"},
35+
"test-platform": {"type": "boolean"},
36+
"publish": {"type": "boolean"},
37+
},
38+
"required": ["features", "arch", "build", "test", "test-platform", "publish"],
39+
},
40+
},
41+
},
42+
"required": ["name", "category", "flavors"],
43+
},
44+
},
45+
},
46+
"required": ["targets"],
47+
}
48+
49+
50+
def find_repo_root():
51+
"""Finds the root directory of the Git repository."""
52+
try:
53+
root = subprocess.check_output(["git", "rev-parse", "--show-toplevel"], text=True).strip()
54+
return root
55+
except subprocess.CalledProcessError:
56+
sys.exit("Error: Unable to determine Git repository root.")
57+
58+
59+
def validate_flavors(data):
60+
"""Validate the flavors.yaml data against the schema."""
61+
try:
62+
validate(instance=data, schema=SCHEMA)
63+
except ValidationError as e:
64+
sys.exit(f"Validation Error: {e.message}")
65+
66+
67+
def should_exclude(combination, excludes, wildcard_excludes):
68+
"""
69+
Checks if a combination should be excluded based on exact match or wildcard patterns.
70+
"""
71+
# Exclude if in explicit excludes
72+
if combination in excludes:
73+
return True
74+
# Exclude if matches any wildcard pattern
75+
return any(fnmatch.fnmatch(combination, pattern) for pattern in wildcard_excludes)
76+
77+
78+
def should_include_only(combination, include_only_patterns):
79+
"""
80+
Checks if a combination should be included based on `--include-only` wildcard patterns.
81+
If no patterns are provided, all combinations are included by default.
82+
"""
83+
if not include_only_patterns:
84+
return True
85+
return any(fnmatch.fnmatch(combination, pattern) for pattern in include_only_patterns)
86+
87+
88+
def parse_flavors(
89+
data,
90+
include_only_patterns=[],
91+
wildcard_excludes=[],
92+
only_build=False,
93+
only_test=False,
94+
only_test_platform=False,
95+
only_publish=False,
96+
filter_categories=[],
97+
exclude_categories=[]
98+
):
99+
"""Parses the flavors.yaml file and generates combinations."""
100+
combinations = [] # Use a list for consistent order
101+
102+
for target in data['targets']:
103+
name = target['name']
104+
category = target.get('category', '')
105+
106+
# Apply category filters
107+
if filter_categories and category not in filter_categories:
108+
continue
109+
if exclude_categories and category in exclude_categories:
110+
continue
111+
112+
for flavor in target['flavors']:
113+
features = flavor.get('features', [])
114+
arch = flavor.get('arch', 'amd64')
115+
build = flavor.get('build', False)
116+
test = flavor.get('test', False)
117+
test_platform = flavor.get('test-platform', False)
118+
publish = flavor.get('publish', False)
119+
120+
# Apply flag-specific filters in the order: build, test, test-platform, publish
121+
if only_build and not build:
122+
continue
123+
if only_test and not test:
124+
continue
125+
if only_test_platform and not test_platform:
126+
continue
127+
if only_publish and not publish:
128+
continue
129+
130+
# Process features
131+
formatted_features = f"-{'-'.join(features)}" if features else ""
132+
133+
# Construct the combination
134+
combination = f"{name}-{formatted_features}-{arch}"
135+
136+
# Format the combination to clean up "--" and "-_"
137+
combination = combination.replace("--", "-").replace("-_", "_")
138+
139+
# Exclude combinations explicitly
140+
if should_exclude(combination, [], wildcard_excludes):
141+
continue
142+
143+
# Apply include-only filters
144+
if not should_include_only(combination, include_only_patterns):
145+
continue
146+
147+
combinations.append((arch, combination))
148+
149+
return sorted(combinations, key=lambda x: x[1].split("-")[0]) # Sort by platform name
150+
151+
152+
def group_by_arch(combinations):
153+
"""Groups combinations by architecture into a JSON dictionary."""
154+
arch_dict = {}
155+
for arch, combination in combinations:
156+
arch_dict.setdefault(arch, []).append(combination)
157+
for arch in arch_dict:
158+
arch_dict[arch] = sorted(set(arch_dict[arch])) # Deduplicate and sort
159+
return arch_dict
160+
161+
162+
def remove_arch(combinations):
163+
"""Removes the architecture from combinations."""
164+
return [combination.replace(f"-{arch}", "") for arch, combination in combinations]
165+
166+
167+
def generate_markdown_table(combinations, no_arch):
168+
"""Generate a markdown table of platforms and their flavors."""
169+
table = "| Platform | Architecture | Flavor |\n"
170+
table += "|------------|--------------------|------------------------------------------|\n"
171+
172+
for arch, combination in combinations:
173+
platform = combination.split("-")[0]
174+
table += f"| {platform:<10} | {arch:<18} | `{combination}` |\n"
175+
176+
return table
177+
178+
179+
if __name__ == "__main__":
180+
parser = argparse.ArgumentParser(description="Parse flavors.yaml and generate combinations.")
181+
parser.add_argument("--no-arch", action="store_true", help="Exclude architecture from the flavor output.")
182+
parser.add_argument(
183+
"--include-only",
184+
action="append",
185+
help="Restrict combinations to those matching wildcard patterns (can be specified multiple times)."
186+
)
187+
parser.add_argument(
188+
"--exclude",
189+
action="append",
190+
help="Exclude combinations based on wildcard patterns (can be specified multiple times)."
191+
)
192+
parser.add_argument(
193+
"--build",
194+
action="store_true",
195+
help="Filter combinations to include only those with build enabled."
196+
)
197+
parser.add_argument(
198+
"--test",
199+
action="store_true",
200+
help="Filter combinations to include only those with test enabled."
201+
)
202+
parser.add_argument(
203+
"--test-platform",
204+
action="store_true",
205+
help="Filter combinations to include only platforms with test-platform: true."
206+
)
207+
parser.add_argument(
208+
"--publish",
209+
action="store_true",
210+
help="Filter combinations to include only those with publish enabled."
211+
)
212+
parser.add_argument(
213+
"--category",
214+
action="append",
215+
help="Filter combinations to include only platforms belonging to the specified categories (can be specified multiple times)."
216+
)
217+
parser.add_argument(
218+
"--exclude-category",
219+
action="append",
220+
help="Exclude platforms belonging to the specified categories (can be specified multiple times)."
221+
)
222+
parser.add_argument(
223+
"--json-by-arch",
224+
action="store_true",
225+
help="Output a JSON dictionary where keys are architectures and values are lists of flavors."
226+
)
227+
parser.add_argument(
228+
"--markdown-table-by-platform",
229+
action="store_true",
230+
help="Generate a markdown table by platform."
231+
)
232+
args = parser.parse_args()
233+
234+
repo_root = find_repo_root()
235+
flavors_file = os.path.join(repo_root, 'flavors.yaml')
236+
if not os.path.isfile(flavors_file):
237+
sys.exit(f"Error: {flavors_file} does not exist.")
238+
239+
# Load and validate the flavors.yaml
240+
with open(flavors_file, 'r') as file:
241+
flavors_data = yaml.safe_load(file)
242+
validate_flavors(flavors_data)
243+
244+
combinations = parse_flavors(
245+
flavors_data,
246+
include_only_patterns=args.include_only or [],
247+
wildcard_excludes=args.exclude or [],
248+
only_build=args.build,
249+
only_test=args.test,
250+
only_test_platform=args.test_platform,
251+
only_publish=args.publish,
252+
filter_categories=args.category or [],
253+
exclude_categories=args.exclude_category or []
254+
)
255+
256+
if args.json_by_arch:
257+
grouped_combinations = group_by_arch(combinations)
258+
# If --no-arch, strip architectures from the grouped output
259+
if args.no_arch:
260+
grouped_combinations = {
261+
arch: sorted(set(item.replace(f"-{arch}", "") for item in items))
262+
for arch, items in grouped_combinations.items()
263+
}
264+
print(json.dumps(grouped_combinations, indent=2))
265+
elif args.markdown_table_by_platform:
266+
markdown_table = generate_markdown_table(combinations, args.no_arch)
267+
print(markdown_table)
268+
else:
269+
if args.no_arch:
270+
no_arch_combinations = remove_arch(combinations)
271+
print("\n".join(sorted(set(no_arch_combinations))))
272+
else:
273+
print("\n".join(sorted(set(comb[1] for comb in combinations))))

0 commit comments

Comments
 (0)