Skip to content

Commit 25a97d7

Browse files
committed
add parsing of flavors
1 parent ed490bb commit 25a97d7

File tree

2 files changed

+271
-0
lines changed

2 files changed

+271
-0
lines changed

src/python_gardenlinux_lib/flavors/__init__.py

Whitespace-only changes.
Lines changed: 271 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,271 @@
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+
no_arch=True,
91+
include_only_patterns=[],
92+
wildcard_excludes=[],
93+
only_build=False,
94+
only_test=False,
95+
only_test_platform=False,
96+
only_publish=False,
97+
filter_categories=[],
98+
exclude_categories=[]
99+
):
100+
"""Parses the flavors.yaml file and generates combinations."""
101+
combinations = [] # Use a list for consistent order
102+
103+
for target in data['targets']:
104+
name = target['name']
105+
category = target.get('category', '')
106+
107+
# Apply category filters
108+
if filter_categories and category not in filter_categories:
109+
continue
110+
if exclude_categories and category in exclude_categories:
111+
continue
112+
113+
for flavor in target['flavors']:
114+
features = flavor.get('features', [])
115+
arch = flavor.get('arch', 'amd64')
116+
build = flavor.get('build', False)
117+
test = flavor.get('test', False)
118+
test_platform = flavor.get('test-platform', False)
119+
publish = flavor.get('publish', False)
120+
121+
# Apply flag-specific filters in the order: build, test, test-platform, publish
122+
if only_build and not build:
123+
continue
124+
if only_test and not test:
125+
continue
126+
if only_test_platform and not test_platform:
127+
continue
128+
if only_publish and not publish:
129+
continue
130+
131+
# Process features
132+
formatted_features = f"-{'-'.join(features)}" if features else ""
133+
134+
# Construct the combination
135+
base_combination = f"{name}{formatted_features}"
136+
137+
# Format the combination to clean up "--" and "-_"
138+
base_combination = base_combination.replace("--", "-").replace("-_", "_")
139+
140+
# Add architecture if requested
141+
if no_arch:
142+
combination = f"{base_combination}-{arch}"
143+
combinations.append((arch, combination))
144+
else:
145+
combinations.append((arch, base_combination))
146+
147+
return sorted(combinations, key=lambda x: x[1].split("-")[0]) # Sort by platform name
148+
149+
150+
def group_by_arch(combinations):
151+
"""Groups combinations by architecture into a JSON dictionary."""
152+
arch_dict = {}
153+
for arch, combination in combinations:
154+
arch_dict.setdefault(arch, []).append(combination)
155+
for arch in arch_dict:
156+
arch_dict[arch] = sorted(set(arch_dict[arch])) # Deduplicate and sort
157+
return arch_dict
158+
159+
160+
def remove_arch(combinations):
161+
"""Removes the architecture from combinations."""
162+
return [combination.replace(f"-{arch}", "") for arch, combination in combinations]
163+
164+
165+
def generate_markdown_table(combinations, no_arch):
166+
"""Generate a markdown table of platforms and their flavors."""
167+
table = "| Platform | Architecture | Flavor |\n"
168+
table += "|------------|--------------------|------------------------------------------|\n"
169+
170+
for arch, combination in combinations:
171+
platform = combination.split("-")[0]
172+
table += f"| {platform:<10} | {arch:<18} | `{combination}` |\n"
173+
174+
return table
175+
176+
177+
if __name__ == "__main__":
178+
parser = argparse.ArgumentParser(description="Parse flavors.yaml and generate combinations.")
179+
parser.add_argument("--no-arch", action="store_true", help="Exclude architecture from the flavor output.")
180+
parser.add_argument(
181+
"--include-only",
182+
action="append",
183+
help="Restrict combinations to those matching wildcard patterns (can be specified multiple times)."
184+
)
185+
parser.add_argument(
186+
"--exclude",
187+
action="append",
188+
help="Exclude combinations based on wildcard patterns (can be specified multiple times)."
189+
)
190+
parser.add_argument(
191+
"--build",
192+
action="store_true",
193+
help="Filter combinations to include only those with build enabled."
194+
)
195+
parser.add_argument(
196+
"--test",
197+
action="store_true",
198+
help="Filter combinations to include only those with test enabled."
199+
)
200+
parser.add_argument(
201+
"--test-platform",
202+
action="store_true",
203+
help="Filter combinations to include only platforms with test-platform: true."
204+
)
205+
parser.add_argument(
206+
"--publish",
207+
action="store_true",
208+
help="Filter combinations to include only those with publish enabled."
209+
)
210+
parser.add_argument(
211+
"--category",
212+
action="append",
213+
help="Filter combinations to include only platforms belonging to the specified categories (can be specified multiple times)."
214+
)
215+
parser.add_argument(
216+
"--exclude-category",
217+
action="append",
218+
help="Exclude platforms belonging to the specified categories (can be specified multiple times)."
219+
)
220+
parser.add_argument(
221+
"--json-by-arch",
222+
action="store_true",
223+
help="Output a JSON dictionary where keys are architectures and values are lists of flavors."
224+
)
225+
parser.add_argument(
226+
"--markdown-table-by-platform",
227+
action="store_true",
228+
help="Generate a markdown table by platform."
229+
)
230+
args = parser.parse_args()
231+
232+
repo_root = find_repo_root()
233+
flavors_file = os.path.join(repo_root, 'flavors.yaml')
234+
if not os.path.isfile(flavors_file):
235+
sys.exit(f"Error: {flavors_file} does not exist.")
236+
237+
# Load and validate the flavors.yaml
238+
with open(flavors_file, 'r') as file:
239+
flavors_data = yaml.safe_load(file)
240+
validate_flavors(flavors_data)
241+
242+
combinations = parse_flavors(
243+
flavors_data,
244+
include_only_patterns=args.include_only or [],
245+
wildcard_excludes=args.exclude or [],
246+
only_build=args.build,
247+
only_test=args.test,
248+
only_test_platform=args.test_platform,
249+
only_publish=args.publish,
250+
filter_categories=args.category or [],
251+
exclude_categories=args.exclude_category or []
252+
)
253+
254+
if args.json_by_arch:
255+
grouped_combinations = group_by_arch(combinations)
256+
# If --no-arch, strip architectures from the grouped output
257+
if args.no_arch:
258+
grouped_combinations = {
259+
arch: sorted(set(item.replace(f"-{arch}", "") for item in items))
260+
for arch, items in grouped_combinations.items()
261+
}
262+
print(json.dumps(grouped_combinations, indent=2))
263+
elif args.markdown_table_by_platform:
264+
markdown_table = generate_markdown_table(combinations, args.no_arch)
265+
print(markdown_table)
266+
else:
267+
if args.no_arch:
268+
no_arch_combinations = remove_arch(combinations)
269+
print("\n".join(sorted(set(no_arch_combinations))))
270+
else:
271+
print("\n".join(sorted(set(comb[1] for comb in combinations))))

0 commit comments

Comments
 (0)