Skip to content

Commit 049e7ea

Browse files
committed
Add license_builder SCons builder function
Add env.to_raw_cstring helper Add env.to_escaped_cstring helper Add env.Run helper Add env.CommandNoCache helper
1 parent 9b8d87d commit 049e7ea

File tree

2 files changed

+247
-0
lines changed

2 files changed

+247
-0
lines changed

SConstruct

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@
44
import os
55
import platform
66
import sys
7+
from typing import List, Union
78

89
import SCons
910

1011
# Local
1112
from build.option_handler import OptionsClass
1213
from build.glob_recursive import GlobRecursive
1314
from build.git_info import get_git_info
15+
from build.license_info import license_builder
1416
from build.cache import show_progress
1517

1618
def normalize_path(val, env):
@@ -273,5 +275,83 @@ env.SetupOptions = SetupOptions
273275
env.FinalizeOptions = FinalizeOptions
274276
env.GlobRecursive = GlobRecursive
275277
env.get_git_info = get_git_info
278+
env.license_builder = license_builder
279+
280+
def to_raw_cstring(value: Union[str, List[str]]) -> str:
281+
MAX_LITERAL = 35 * 1024
282+
283+
if isinstance(value, list):
284+
value = "\n".join(value) + "\n"
285+
286+
split: List[bytes] = []
287+
offset = 0
288+
encoded = value.encode()
289+
290+
while offset <= len(encoded):
291+
segment = encoded[offset : offset + MAX_LITERAL]
292+
offset += MAX_LITERAL
293+
if len(segment) == MAX_LITERAL:
294+
# Try to segment raw strings at double newlines to keep readable.
295+
pretty_break = segment.rfind(b"\n\n")
296+
if pretty_break != -1:
297+
segment = segment[: pretty_break + 1]
298+
offset -= MAX_LITERAL - pretty_break - 1
299+
# If none found, ensure we end with valid utf8.
300+
# https://github.com/halloleo/unicut/blob/master/truncate.py
301+
elif segment[-1] & 0b10000000:
302+
last_11xxxxxx_index = [i for i in range(-1, -5, -1) if segment[i] & 0b11000000 == 0b11000000][0]
303+
last_11xxxxxx = segment[last_11xxxxxx_index]
304+
if not last_11xxxxxx & 0b00100000:
305+
last_char_length = 2
306+
elif not last_11xxxxxx & 0b0010000:
307+
last_char_length = 3
308+
elif not last_11xxxxxx & 0b0001000:
309+
last_char_length = 4
310+
311+
if last_char_length > -last_11xxxxxx_index:
312+
segment = segment[:last_11xxxxxx_index]
313+
offset += last_11xxxxxx_index
314+
315+
split += [segment]
316+
317+
if len(split) == 1:
318+
return f'R"<!>({split[0].decode()})<!>"'
319+
else:
320+
# Wrap multiple segments in parenthesis to suppress `string-concatenation` warnings on clang.
321+
return "({})".format(" ".join(f'R"<!>({segment.decode()})<!>"' for segment in split))
322+
323+
324+
C_ESCAPABLES = [
325+
("\\", "\\\\"),
326+
("\a", "\\a"),
327+
("\b", "\\b"),
328+
("\f", "\\f"),
329+
("\n", "\\n"),
330+
("\r", "\\r"),
331+
("\t", "\\t"),
332+
("\v", "\\v"),
333+
# ("'", "\\'"), # Skip, as we're only dealing with full strings.
334+
('"', '\\"'),
335+
]
336+
C_ESCAPE_TABLE = str.maketrans(dict((x, y) for x, y in C_ESCAPABLES))
337+
338+
def to_escaped_cstring(value: str) -> str:
339+
return value.translate(C_ESCAPE_TABLE)
340+
341+
def Run(env, function, **kwargs):
342+
return SCons.Action.Action(function, "$GENCOMSTR", **kwargs)
343+
344+
def CommandNoCache(env, target, sources, command, **kwargs):
345+
result = env.Command(target, sources, command, **kwargs)
346+
env.NoCache(result)
347+
for key, val in kwargs.items():
348+
env.Depends(result, env.Value({ key: val }))
349+
return result
350+
351+
env.to_raw_cstring = to_raw_cstring
352+
env.to_escaped_cstring = to_escaped_cstring
353+
354+
env.__class__.Run = Run
355+
env.__class__.CommandNoCache = CommandNoCache
276356

277357
Return("env")

build/license_info.py

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
from collections import OrderedDict
2+
from io import TextIOWrapper
3+
4+
5+
def get_license_info(src_copyright):
6+
class LicenseReader:
7+
def __init__(self, license_file: TextIOWrapper):
8+
self._license_file = license_file
9+
self.line_num = 0
10+
self.current = self.next_line()
11+
12+
def next_line(self):
13+
line = self._license_file.readline()
14+
self.line_num += 1
15+
while line.startswith("#"):
16+
line = self._license_file.readline()
17+
self.line_num += 1
18+
self.current = line
19+
return line
20+
21+
def next_tag(self):
22+
if ":" not in self.current:
23+
return ("", [])
24+
tag, line = self.current.split(":", 1)
25+
lines = [line.strip()]
26+
while self.next_line() and self.current.startswith(" "):
27+
lines.append(self.current.strip())
28+
return (tag, lines)
29+
30+
projects = OrderedDict()
31+
license_list = []
32+
33+
with open(src_copyright, "r", encoding="utf-8") as copyright_file:
34+
reader = LicenseReader(copyright_file)
35+
part = {}
36+
while reader.current:
37+
tag, content = reader.next_tag()
38+
if tag in ("Files", "Copyright", "License"):
39+
part[tag] = content[:]
40+
elif tag == "Comment" and part:
41+
# attach non-empty part to named project
42+
projects[content[0]] = projects.get(content[0], []) + [part]
43+
44+
if not tag or not reader.current:
45+
# end of a paragraph start a new part
46+
if "License" in part and "Files" not in part:
47+
# no Files tag in this one, so assume standalone license
48+
license_list.append(part["License"])
49+
part = {}
50+
reader.next_line()
51+
52+
data_list: list = []
53+
for project in iter(projects.values()):
54+
for part in project:
55+
part["file_index"] = len(data_list)
56+
data_list += part["Files"]
57+
part["copyright_index"] = len(data_list)
58+
data_list += part["Copyright"]
59+
60+
return {"data": data_list, "projects": projects, "parts": part, "licenses": license_list}
61+
62+
63+
def license_builder(target, source, env):
64+
name_prefix = env.get("name_prefix", "project")
65+
prefix_upper = name_prefix.upper()
66+
prefix_capital = name_prefix.capitalize()
67+
68+
license_text_name = f"{prefix_upper}_LICENSE_TEXT"
69+
component_copyright_part_name = f"{prefix_capital}ComponentCopyrightPart"
70+
component_copyright_name = f"{prefix_capital}ComponentCopyright"
71+
copyright_data_name = f"{prefix_upper}_COPYRIGHT_DATA"
72+
copyright_parts_name = f"{prefix_upper}_COPYRIGHT_PARTS"
73+
copyright_info_name = f"{prefix_upper}_COPYRIGHT_INFO"
74+
license_name = f"{prefix_capital}License"
75+
licenses_name = f"{prefix_upper}_LICENSES"
76+
77+
src_copyright = get_license_info(str(source[0]))
78+
src_license = str(source[1])
79+
80+
with open(src_license, "r", encoding="utf-8") as file:
81+
license_text = file.read()
82+
83+
def copyright_data_str() -> str:
84+
result = ""
85+
for line in src_copyright["data"]:
86+
result += f'\t\t"{line}",\n'
87+
return result
88+
89+
part_indexes = {}
90+
91+
def copyright_part_str() -> str:
92+
part_index = 0
93+
result = ""
94+
for project_name, project in iter(src_copyright["projects"].items()):
95+
part_indexes[project_name] = part_index
96+
for part in project:
97+
result += (
98+
f'\t\t{{ "{env.to_escaped_cstring(part["License"][0])}", '
99+
+ f"{{ &{copyright_data_name}[{part['file_index']}], {len(part['Files'])} }}, "
100+
+ f"{{ &{copyright_data_name}[{part['copyright_index']}], {len(part['Copyright'])} }} }},\n"
101+
)
102+
part_index += 1
103+
return result
104+
105+
def copyright_info_str() -> str:
106+
result = ""
107+
for project_name, project in iter(src_copyright["projects"].items()):
108+
result += (
109+
f'\t\t{{ "{env.to_escaped_cstring(project_name)}", '
110+
+ f"{{ &{copyright_parts_name}[{part_indexes[project_name]}], {len(project)} }} }},\n"
111+
)
112+
return result
113+
114+
def license_list_str() -> str:
115+
result = ""
116+
for license in iter(src_copyright["licenses"]):
117+
result += (
118+
f'\t\t{{ "{env.to_escaped_cstring(license[0])}",'
119+
+ f'\n\t\t {env.to_raw_cstring([line if line != "." else "" for line in license[1:]])} }}, \n'
120+
)
121+
return result
122+
123+
with open(str(target[0]), "wt", encoding="utf-8", newline="\n") as file:
124+
file.write("/* THIS FILE IS GENERATED. EDITS WILL BE LOST. */\n\n")
125+
file.write(
126+
f"""\
127+
#pragma once
128+
129+
#include <array>
130+
#include <span>
131+
#include <string_view>
132+
133+
namespace OpenVic {{
134+
static constexpr std::string_view {license_text_name} = {{
135+
{env.to_raw_cstring(license_text)}
136+
}};
137+
138+
struct {component_copyright_part_name} {{
139+
std::string_view license;
140+
std::span<const std::string_view> files;
141+
std::span<const std::string_view> copyright_statements;
142+
}};
143+
144+
struct {component_copyright_name} {{
145+
std::string_view name;
146+
std::span<const {component_copyright_part_name}> parts;
147+
}};
148+
149+
static constexpr std::array {copyright_data_name} = std::to_array<std::string_view>({{
150+
{copyright_data_str()}\t}});
151+
152+
static constexpr std::array {copyright_parts_name} = std::to_array<{component_copyright_part_name}>({{
153+
{copyright_part_str()}\t}});
154+
155+
static constexpr std::array {copyright_info_name} = std::to_array<{component_copyright_name}>({{
156+
{copyright_info_str()}\t}});
157+
158+
struct {license_name} {{
159+
std::string_view license_name;
160+
std::string_view license_body;
161+
}};
162+
163+
static constexpr std::array {licenses_name} = std::to_array<{license_name}>({{
164+
{license_list_str()}\t}});
165+
}}
166+
"""
167+
)

0 commit comments

Comments
 (0)