cja 0.2.2__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: cja
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: CMake-compatible build system that generates Ninja build files
5
5
  Author: Jan Niklas Hasse
6
6
  Author-email: Jan Niklas Hasse <jhasse@bixense.com>
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "cja"
3
- version = "0.2.2"
3
+ version = "0.2.4"
4
4
  description = "CMake-compatible build system that generates Ninja build files"
5
5
  readme = "README.md"
6
6
  license = { text = "GPL-3.0-or-later" }
@@ -47,6 +47,21 @@ class CustomCommand:
47
47
  defined_line: int = 0
48
48
 
49
49
 
50
+ @dataclass
51
+ class CustomTarget:
52
+ """A custom target (phony build target)."""
53
+
54
+ name: str
55
+ commands: list[list[str]]
56
+ depends: list[str]
57
+ all: bool = False
58
+ working_directory: str | None = None
59
+ verbatim: bool = False
60
+ comment: str = ""
61
+ defined_file: Path | None = None
62
+ defined_line: int = 0
63
+
64
+
50
65
  @dataclass
51
66
  class BuildContext:
52
67
  """Context for processing CMake commands."""
@@ -58,7 +73,9 @@ class BuildContext:
58
73
  project_name: str = ""
59
74
  variables: dict[str, str] = field(default_factory=dict)
60
75
  cache_variables: set[str] = field(default_factory=set) # Variables from -D flags
61
- cli_variables: dict[str, str] = field(default_factory=dict) # Original -D flag values
76
+ cli_variables: dict[str, str] = field(
77
+ default_factory=dict
78
+ ) # Original -D flag values
62
79
  libraries: list[Library] = field(default_factory=list)
63
80
  executables: list[Executable] = field(default_factory=list)
64
81
  imported_targets: dict[str, ImportedTarget] = field(default_factory=dict)
@@ -69,6 +86,9 @@ class BuildContext:
69
86
  custom_commands: list[CustomCommand] = field(
70
87
  default_factory=list
71
88
  ) # Custom build commands
89
+ custom_targets: list[CustomTarget] = field(
90
+ default_factory=list
91
+ ) # Custom targets (phony)
72
92
  functions: dict[str, FunctionDef] = field(
73
93
  default_factory=dict
74
94
  ) # User-defined functions
@@ -116,6 +136,8 @@ class BuildContext:
116
136
  def get_library(self, name: str) -> Library | None:
117
137
  for lib in self.libraries:
118
138
  if lib.name == name:
139
+ if lib.is_alias and lib.alias_for:
140
+ return self.get_library(lib.alias_for)
119
141
  return lib
120
142
  return None
121
143
 
@@ -179,7 +201,9 @@ class BuildContext:
179
201
  if allow_undefined_empty:
180
202
  return ""
181
203
  if allow_undefined_warning:
182
- self.print_warning(f"undefined variable referenced: {var_name}", line)
204
+ self.print_warning(
205
+ f"undefined variable referenced: {var_name}", line
206
+ )
183
207
  return ""
184
208
  level = self.print_error if strict else self.print_warning
185
209
  level(f"undefined variable referenced: {var_name}", line)
@@ -21,6 +21,7 @@ from .parser import Command
21
21
  from .targets import Executable, ImportedTarget, Library
22
22
  from .utils import (
23
23
  UNDEFINED_VAR_SENTINEL,
24
+ cmake_regex_to_python,
24
25
  is_truthy,
25
26
  resolve_cmake_path,
26
27
  strip_generator_expressions,
@@ -67,7 +68,15 @@ def handle_project(
67
68
  ctx.variables["PROJECT_NAME"] = args[0]
68
69
  ctx.variables["CMAKE_PROJECT_NAME"] = args[0]
69
70
  ctx.variables["CMAKE_C_FLAGS"] = "" # TODO: Only set when C is enabled
71
+ ctx.variables["CMAKE_C_FLAGS_DEBUG"] = ""
72
+ ctx.variables["CMAKE_C_FLAGS_RELEASE"] = ""
73
+ ctx.variables["CMAKE_C_FLAGS_RELWITHDEBINFO"] = ""
74
+ ctx.variables["CMAKE_C_FLAGS_MINSIZEREL"] = ""
70
75
  ctx.variables["CMAKE_CXX_FLAGS"] = "" # TODO: Only set when CXX is enabled
76
+ ctx.variables["CMAKE_CXX_FLAGS_DEBUG"] = ""
77
+ ctx.variables["CMAKE_CXX_FLAGS_RELEASE"] = ""
78
+ ctx.variables["CMAKE_CXX_FLAGS_RELWITHDEBINFO"] = ""
79
+ ctx.variables["CMAKE_CXX_FLAGS_MINSIZEREL"] = ""
71
80
  ctx.variables["PROJECT_SOURCE_DIR"] = str(ctx.current_source_dir)
72
81
  ctx.variables["PROJECT_BINARY_DIR"] = str(ctx.build_dir)
73
82
  source_var = f"{args[0]}_SOURCE_DIR"
@@ -185,41 +194,42 @@ def handle_target_link_libraries(
185
194
  elif arg == "PRIVATE":
186
195
  visibility = "PRIVATE"
187
196
  else:
188
- # It's a library name
197
+ # It's a library name (may be a semicolon-separated list from
198
+ # a CMake variable expansion like "${LIBS_PRIVATE}")
189
199
  if "$<" in arg:
190
200
  arg = strip_generator_expressions(arg, ctx.variables)
191
201
  if not arg:
192
202
  continue
193
203
 
194
- lib = ctx.get_library(target_name)
195
- if lib:
196
- # For libraries, we track linked libraries but don't use them yet
197
- # (static libraries don't link, but they might need to propagate flags)
198
- if visibility == "PUBLIC":
199
- lib.link_libraries.append(arg)
200
- lib.public_link_libraries.append(arg)
201
- elif visibility == "INTERFACE":
202
- lib.public_link_libraries.append(arg)
203
- else:
204
- lib.link_libraries.append(arg)
205
- else:
206
- exe = ctx.get_executable(target_name)
207
- if exe:
208
- exe.link_libraries.append(arg)
209
- elif target_name in ctx.imported_targets:
210
- imported_target = ctx.imported_targets[target_name]
211
- existing = shlex.split(imported_target.libs) if imported_target.libs else []
212
- if arg in ctx.imported_targets and ctx.imported_targets[arg].libs:
213
- existing.extend(shlex.split(ctx.imported_targets[arg].libs))
214
- elif (
215
- arg.startswith("-")
216
- or "/" in arg
217
- or arg.endswith((".a", ".so", ".dylib", ".lib", ".dll"))
218
- ):
219
- existing.append(arg)
204
+ parts = [p for p in arg.split(";") if p]
205
+ for part in parts:
206
+ lib = ctx.get_library(target_name)
207
+ if lib:
208
+ if visibility == "PUBLIC":
209
+ lib.link_libraries.append(part)
210
+ lib.public_link_libraries.append(part)
211
+ elif visibility == "INTERFACE":
212
+ lib.public_link_libraries.append(part)
220
213
  else:
221
- existing.append(f"-l{arg}")
222
- imported_target.libs = " ".join(dict.fromkeys(existing))
214
+ lib.link_libraries.append(part)
215
+ else:
216
+ exe = ctx.get_executable(target_name)
217
+ if exe:
218
+ exe.link_libraries.append(part)
219
+ elif target_name in ctx.imported_targets:
220
+ imported_target = ctx.imported_targets[target_name]
221
+ existing = shlex.split(imported_target.libs) if imported_target.libs else []
222
+ if part in ctx.imported_targets and ctx.imported_targets[part].libs:
223
+ existing.extend(shlex.split(ctx.imported_targets[part].libs))
224
+ elif (
225
+ part.startswith("-")
226
+ or "/" in part
227
+ or part.endswith((".a", ".so", ".dylib", ".lib", ".dll"))
228
+ ):
229
+ existing.append(part)
230
+ else:
231
+ existing.append(f"-l{part}")
232
+ imported_target.libs = " ".join(dict.fromkeys(existing))
223
233
 
224
234
 
225
235
  def handle_target_link_directories(
@@ -1442,15 +1452,16 @@ def handle_list(
1442
1452
  items = list_val.split(";")
1443
1453
  import re as regex_module
1444
1454
 
1455
+ py_pattern = cmake_regex_to_python(pattern)
1445
1456
  if mode == "INCLUDE":
1446
1457
  items = [
1447
- item for item in items if regex_module.search(pattern, item)
1458
+ item for item in items if regex_module.search(py_pattern, item)
1448
1459
  ]
1449
1460
  elif mode == "EXCLUDE":
1450
1461
  items = [
1451
1462
  item
1452
1463
  for item in items
1453
- if not regex_module.search(pattern, item)
1464
+ if not regex_module.search(py_pattern, item)
1454
1465
  ]
1455
1466
  ctx.variables[list_name] = ";".join(items)
1456
1467
  else:
@@ -1864,7 +1875,7 @@ def handle_string(
1864
1875
  out_var = args[3]
1865
1876
  inputs = args[4:]
1866
1877
  full_input = "".join(inputs)
1867
- match = re.search(pattern, full_input)
1878
+ match = re.search(cmake_regex_to_python(pattern), full_input)
1868
1879
  reset_cmake_match_vars()
1869
1880
  if match:
1870
1881
  ctx.variables[out_var] = match.group(0)
@@ -1886,7 +1897,10 @@ def handle_string(
1886
1897
  out_var = args[3]
1887
1898
  inputs = args[4:]
1888
1899
  full_input = "".join(inputs)
1889
- matches = re.findall(pattern, full_input)
1900
+ matches = [
1901
+ m.group(0)
1902
+ for m in re.finditer(cmake_regex_to_python(pattern), full_input)
1903
+ ]
1890
1904
  ctx.variables[out_var] = ";".join(matches)
1891
1905
  elif regex_sub == "REPLACE":
1892
1906
  # string(REGEX REPLACE <regex> <replace_string> <out_var> <input> [<input>...])
@@ -1900,7 +1914,7 @@ def handle_string(
1900
1914
  # CMake uses \1, \2 etc for backreferences, Python uses \1, \2
1901
1915
  # but also supports \g<1> which is safer.
1902
1916
  # Actually CMake's REGEX REPLACE is a bit different.
1903
- ctx.variables[out_var] = re.sub(pattern, replace_str, full_input)
1917
+ ctx.variables[out_var] = re.sub(cmake_regex_to_python(pattern), replace_str, full_input)
1904
1918
 
1905
1919
  elif subcommand == "SUBSTRING":
1906
1920
  # string(SUBSTRING <string> <begin> <length> <out_var>)
@@ -1990,6 +2004,16 @@ def handle_string(
1990
2004
  result = s1 > s2
1991
2005
  ctx.variables[out_var] = "1" if result else "0"
1992
2006
 
2007
+ elif subcommand == "MAKE_C_IDENTIFIER":
2008
+ # string(MAKE_C_IDENTIFIER <string> <output_variable>)
2009
+ if len(args) >= 3:
2010
+ string_val = args[1]
2011
+ out_var = args[2]
2012
+ result = re.sub(r"[^a-zA-Z0-9_]", "_", string_val)
2013
+ if result and result[0].isdigit():
2014
+ result = "_" + result
2015
+ ctx.variables[out_var] = result
2016
+
1993
2017
 
1994
2018
  def handle_file(
1995
2019
  ctx: BuildContext,
@@ -0,0 +1,274 @@
1
+ import re
2
+ from .parser import Command
3
+ from .build_context import (
4
+ BuildContext,
5
+ find_matching_endforeach,
6
+ find_matching_endif,
7
+ )
8
+ from .syntax import evaluate_condition, find_else_or_elseif
9
+
10
+
11
+ def select_if_block(
12
+ ctx: BuildContext,
13
+ commands: list[Command],
14
+ pc: int,
15
+ strict: bool,
16
+ ) -> tuple[int, tuple[int, int] | None]:
17
+ """Select the if/elseif/else block to execute."""
18
+ cmd = commands[pc]
19
+ # Find matching endif
20
+ endif_idx = find_matching_endif(commands, pc, ctx)
21
+ # Find elseif/else blocks
22
+ blocks = find_else_or_elseif(commands, pc, endif_idx)
23
+
24
+ block_start = pc + 1
25
+
26
+ def target_exists(name: str) -> bool:
27
+ return (
28
+ ctx.get_library(name) is not None
29
+ or ctx.get_executable(name) is not None
30
+ or name in ctx.imported_targets
31
+ )
32
+
33
+ def _is_empty_string_token(token: str) -> bool:
34
+ return token == "" or token in ('""', "''")
35
+
36
+ def _is_exact_var_token(token: str) -> bool:
37
+ return (
38
+ (token.startswith("${") and token.endswith("}"))
39
+ or (token.startswith('"${') and token.endswith('}"'))
40
+ or (token.startswith("'${") and token.endswith("}'"))
41
+ )
42
+
43
+ # Check the if condition
44
+ if_args = []
45
+ for i, arg in enumerate(cmd.args):
46
+ allow_undefined = (
47
+ i + 2 < len(cmd.args)
48
+ and cmd.args[i + 1] == "STREQUAL"
49
+ and _is_empty_string_token(cmd.args[i + 2])
50
+ and _is_exact_var_token(arg)
51
+ )
52
+ if_args.append(
53
+ ctx.expand_variables(
54
+ arg,
55
+ strict,
56
+ cmd.line,
57
+ allow_undefined_empty=allow_undefined,
58
+ allow_undefined_warning="${${" in arg,
59
+ )
60
+ )
61
+ if evaluate_condition(if_args, ctx.variables, target_exists=target_exists):
62
+ # Execute commands from if to first elseif/else or endif
63
+ block_end = blocks[0][1] if blocks else endif_idx
64
+ return endif_idx, (block_start, block_end)
65
+
66
+ # Check elseif/else blocks
67
+ for j, (block_type, block_idx, block_args) in enumerate(blocks):
68
+ if block_type == "elseif":
69
+ elseif_args = []
70
+ for i, arg in enumerate(block_args):
71
+ allow_undefined = (
72
+ i + 2 < len(block_args)
73
+ and block_args[i + 1] == "STREQUAL"
74
+ and _is_empty_string_token(block_args[i + 2])
75
+ and _is_exact_var_token(arg)
76
+ )
77
+ elseif_args.append(
78
+ ctx.expand_variables(
79
+ arg,
80
+ strict,
81
+ commands[block_idx].line,
82
+ allow_undefined_empty=allow_undefined,
83
+ allow_undefined_warning="${${" in arg,
84
+ )
85
+ )
86
+ if evaluate_condition(
87
+ elseif_args, ctx.variables, target_exists=target_exists
88
+ ):
89
+ block_start = block_idx + 1
90
+ block_end = blocks[j + 1][1] if j + 1 < len(blocks) else endif_idx
91
+ return endif_idx, (block_start, block_end)
92
+ elif block_type == "else":
93
+ block_start = block_idx + 1
94
+ return endif_idx, (block_start, endif_idx)
95
+
96
+ return endif_idx, None
97
+
98
+
99
+ def build_foreach_info(
100
+ ctx: BuildContext,
101
+ commands: list[Command],
102
+ pc: int,
103
+ args: list[str],
104
+ ) -> tuple[int, str, list[str], list[Command]]:
105
+ """Build foreach() iteration info."""
106
+ cmd = commands[pc]
107
+ if not args:
108
+ ctx.raise_syntax_error("foreach() requires at least a loop variable", cmd.line)
109
+
110
+ # Find matching endforeach
111
+ endforeach_idx = find_matching_endforeach(commands, pc, ctx)
112
+ body = commands[pc + 1 : endforeach_idx]
113
+
114
+ loop_var = cmd.args[0] # Use unexpanded for variable name
115
+ remaining = args[1:] # Use expanded args for values
116
+
117
+ # Determine iteration items
118
+ items: list[str] = []
119
+ if remaining and remaining[0] == "RANGE":
120
+ # foreach(var RANGE stop) or foreach(var RANGE start stop [step])
121
+ range_args = remaining[1:]
122
+ if len(range_args) == 1:
123
+ stop = int(range_args[0])
124
+ items = [str(x) for x in range(stop + 1)]
125
+ elif len(range_args) == 2:
126
+ start, stop = int(range_args[0]), int(range_args[1])
127
+ items = [str(x) for x in range(start, stop + 1)]
128
+ elif len(range_args) >= 3:
129
+ start, stop, step = (
130
+ int(range_args[0]),
131
+ int(range_args[1]),
132
+ int(range_args[2]),
133
+ )
134
+ items = [str(x) for x in range(start, stop + 1, step)]
135
+ elif remaining and remaining[0] == "IN":
136
+ # foreach(var IN LISTS list1 ... | ITEMS item1 ...)
137
+ mode = remaining[1] if len(remaining) > 1 else ""
138
+ values = remaining[2:]
139
+ if mode == "LISTS":
140
+ for list_name in values:
141
+ list_val = ctx.variables.get(list_name, "")
142
+ if list_val:
143
+ items.extend(list_val.split())
144
+ elif mode == "ITEMS":
145
+ items = values
146
+ else:
147
+ # foreach(var item1 item2 ...)
148
+ items = remaining
149
+
150
+ return endforeach_idx, loop_var, items, body
151
+
152
+
153
+ _VERSION_RE = re.compile(r"^(\d+)(?:\.(\d+))?(?:\.(\d+))?")
154
+
155
+
156
+ def _version_components(version: str) -> tuple[str, str, str]:
157
+ match = _VERSION_RE.match(version.strip())
158
+ if not match:
159
+ return "0", "0", "0"
160
+ major = match.group(1) or "0"
161
+ minor = match.group(2) or "0"
162
+ patch = match.group(3) or "0"
163
+ return major, minor, patch
164
+
165
+
166
+ def _render_basic_package_version_file(
167
+ version: str,
168
+ compatibility: str,
169
+ arch_independent: bool,
170
+ ctx: BuildContext,
171
+ ) -> str:
172
+ major, minor, _ = _version_components(version)
173
+ arch_block = ""
174
+ if arch_independent:
175
+ arch_block = "set(PACKAGE_VERSION_UNSUITABLE FALSE)\n"
176
+ else:
177
+ sizeof_void_p = ctx.variables.get(
178
+ "CMAKE_SIZEOF_VOID_P", "${CMAKE_SIZEOF_VOID_P}"
179
+ )
180
+ arch_block = (
181
+ f'set(PACKAGE_VERSION_SIZEOF_VOID_P "{sizeof_void_p}")\n'
182
+ "if(NOT CMAKE_SIZEOF_VOID_P STREQUAL PACKAGE_VERSION_SIZEOF_VOID_P)\n"
183
+ " set(PACKAGE_VERSION_UNSUITABLE TRUE)\n"
184
+ "endif()\n"
185
+ )
186
+
187
+ return (
188
+ f'set(PACKAGE_VERSION "{version}")\n'
189
+ "set(PACKAGE_VERSION_COMPATIBLE FALSE)\n"
190
+ "set(PACKAGE_VERSION_EXACT FALSE)\n"
191
+ f"{arch_block}"
192
+ "if(PACKAGE_VERSION VERSION_LESS PACKAGE_FIND_VERSION)\n"
193
+ " set(PACKAGE_VERSION_COMPATIBLE FALSE)\n"
194
+ "else()\n"
195
+ f' if("{compatibility}" STREQUAL "AnyNewerVersion")\n'
196
+ " set(PACKAGE_VERSION_COMPATIBLE TRUE)\n"
197
+ f' elseif("{compatibility}" STREQUAL "SameMajorVersion")\n'
198
+ f' if(PACKAGE_FIND_VERSION_MAJOR STREQUAL "{major}")\n'
199
+ " set(PACKAGE_VERSION_COMPATIBLE TRUE)\n"
200
+ " endif()\n"
201
+ f' elseif("{compatibility}" STREQUAL "SameMinorVersion")\n'
202
+ f' if(PACKAGE_FIND_VERSION_MAJOR STREQUAL "{major}" AND '
203
+ f'PACKAGE_FIND_VERSION_MINOR STREQUAL "{minor}")\n'
204
+ " set(PACKAGE_VERSION_COMPATIBLE TRUE)\n"
205
+ " endif()\n"
206
+ f' elseif("{compatibility}" STREQUAL "ExactVersion")\n'
207
+ " if(PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION)\n"
208
+ " set(PACKAGE_VERSION_COMPATIBLE TRUE)\n"
209
+ " endif()\n"
210
+ " endif()\n"
211
+ "endif()\n"
212
+ "if(PACKAGE_VERSION_COMPATIBLE AND PACKAGE_FIND_VERSION STREQUAL PACKAGE_VERSION)\n"
213
+ " set(PACKAGE_VERSION_EXACT TRUE)\n"
214
+ "endif()\n"
215
+ "if(NOT DEFINED PACKAGE_VERSION_UNSUITABLE)\n"
216
+ " set(PACKAGE_VERSION_UNSUITABLE FALSE)\n"
217
+ "endif()\n"
218
+ )
219
+
220
+
221
+ def _render_package_init_block(
222
+ install_destination: str,
223
+ *,
224
+ no_set_and_check_macro: bool,
225
+ no_check_required_components_macro: bool,
226
+ ) -> str:
227
+ install_destination = install_destination.replace("\\", "/").strip()
228
+ if install_destination.startswith("/"):
229
+ prefix_expr = install_destination
230
+ else:
231
+ parts = [p for p in install_destination.split("/") if p and p != "."]
232
+ up = "/".join([".."] * len(parts))
233
+ prefix_expr = (
234
+ "${CMAKE_CURRENT_LIST_DIR}"
235
+ if not up
236
+ else f"${{CMAKE_CURRENT_LIST_DIR}}/{up}"
237
+ )
238
+
239
+ lines = [
240
+ "# Generated by cja: configure_package_config_file()",
241
+ f'get_filename_component(PACKAGE_PREFIX_DIR "{prefix_expr}" ABSOLUTE)',
242
+ "",
243
+ ]
244
+
245
+ if not no_set_and_check_macro:
246
+ lines.extend(
247
+ [
248
+ "macro(set_and_check _var _file)",
249
+ ' set(${_var} "${_file}")',
250
+ ' if(NOT EXISTS "${_file}")',
251
+ ' message(FATAL_ERROR "File or directory ${_file} referenced by ${_var} does not exist")',
252
+ " endif()",
253
+ "endmacro()",
254
+ "",
255
+ ]
256
+ )
257
+
258
+ if not no_check_required_components_macro:
259
+ lines.extend(
260
+ [
261
+ "macro(check_required_components _NAME)",
262
+ " foreach(comp ${${_NAME}_FIND_COMPONENTS})",
263
+ " if(NOT ${_NAME}_${comp}_FOUND)",
264
+ " if(${_NAME}_FIND_REQUIRED_${comp})",
265
+ " set(${_NAME}_FOUND FALSE)",
266
+ " endif()",
267
+ " endif()",
268
+ " endforeach()",
269
+ "endmacro()",
270
+ "",
271
+ ]
272
+ )
273
+
274
+ return "\n".join(lines)