xref: /aosp_15_r20/external/zstd/contrib/freestanding_lib/freestanding.py (revision 01826a4963a0d8a59bc3812d29bdf0fb76416722)
1#!/usr/bin/env python3
2# ################################################################
3# Copyright (c) Meta Platforms, Inc. and affiliates.
4# All rights reserved.
5#
6# This source code is licensed under both the BSD-style license (found in the
7# LICENSE file in the root directory of this source tree) and the GPLv2 (found
8# in the COPYING file in the root directory of this source tree).
9# You may select, at your option, one of the above-listed licenses.
10# ##########################################################################
11
12import argparse
13import contextlib
14import os
15import re
16import shutil
17import sys
18from typing import Optional
19
20
21INCLUDED_SUBDIRS = ["common", "compress", "decompress"]
22
23SKIPPED_FILES = [
24    "common/mem.h",
25    "common/zstd_deps.h",
26    "common/pool.c",
27    "common/pool.h",
28    "common/threading.c",
29    "common/threading.h",
30    "common/zstd_trace.h",
31    "compress/zstdmt_compress.h",
32    "compress/zstdmt_compress.c",
33]
34
35XXHASH_FILES = [
36    "common/xxhash.c",
37    "common/xxhash.h",
38]
39
40
41class FileLines(object):
42    def __init__(self, filename):
43        self.filename = filename
44        with open(self.filename, "r") as f:
45            self.lines = f.readlines()
46
47    def write(self):
48        with open(self.filename, "w") as f:
49            f.write("".join(self.lines))
50
51
52class PartialPreprocessor(object):
53    """
54    Looks for simple ifdefs and ifndefs and replaces them.
55    Handles && and ||.
56    Has fancy logic to handle translating elifs to ifs.
57    Only looks for macros in the first part of the expression with no
58    parens.
59    Does not handle multi-line macros (only looks in first line).
60    """
61    def __init__(self, defs: [(str, Optional[str])], replaces: [(str, str)], undefs: [str]):
62        MACRO_GROUP = r"(?P<macro>[a-zA-Z_][a-zA-Z_0-9]*)"
63        ELIF_GROUP = r"(?P<elif>el)?"
64        OP_GROUP = r"(?P<op>&&|\|\|)?"
65
66        self._defs = {macro:value for macro, value in defs}
67        self._replaces = {macro:value for macro, value in replaces}
68        self._defs.update(self._replaces)
69        self._undefs = set(undefs)
70
71        self._define = re.compile(r"\s*#\s*define")
72        self._if = re.compile(r"\s*#\s*if")
73        self._elif = re.compile(r"\s*#\s*(?P<elif>el)if")
74        self._else = re.compile(r"\s*#\s*(?P<else>else)")
75        self._endif = re.compile(r"\s*#\s*endif")
76
77        self._ifdef = re.compile(fr"\s*#\s*if(?P<not>n)?def {MACRO_GROUP}\s*")
78        self._if_defined = re.compile(
79            fr"\s*#\s*{ELIF_GROUP}if\s+(?P<not>!)?\s*defined\s*\(\s*{MACRO_GROUP}\s*\)\s*{OP_GROUP}"
80        )
81        self._if_defined_value = re.compile(
82            fr"\s*#\s*{ELIF_GROUP}if\s+defined\s*\(\s*{MACRO_GROUP}\s*\)\s*"
83            fr"(?P<op>&&)\s*"
84            fr"(?P<openp>\()?\s*"
85            fr"(?P<macro2>[a-zA-Z_][a-zA-Z_0-9]*)\s*"
86            fr"(?P<cmp>[=><!]+)\s*"
87            fr"(?P<value>[0-9]*)\s*"
88            fr"(?P<closep>\))?\s*"
89        )
90        self._if_true = re.compile(
91            fr"\s*#\s*{ELIF_GROUP}if\s+{MACRO_GROUP}\s*{OP_GROUP}"
92        )
93
94        self._c_comment = re.compile(r"/\*.*?\*/")
95        self._cpp_comment = re.compile(r"//")
96
97    def _log(self, *args, **kwargs):
98        print(*args, **kwargs)
99
100    def _strip_comments(self, line):
101        # First strip c-style comments (may include //)
102        while True:
103            m = self._c_comment.search(line)
104            if m is None:
105                break
106            line = line[:m.start()] + line[m.end():]
107
108        # Then strip cpp-style comments
109        m = self._cpp_comment.search(line)
110        if m is not None:
111            line = line[:m.start()]
112
113        return line
114
115    def _fixup_indentation(self, macro, replace: [str]):
116        if len(replace) == 0:
117            return replace
118        if len(replace) == 1 and self._define.match(replace[0]) is None:
119            # If there is only one line, only replace defines
120            return replace
121
122
123        all_pound = True
124        for line in replace:
125            if not line.startswith('#'):
126                all_pound = False
127        if all_pound:
128            replace = [line[1:] for line in replace]
129
130        min_spaces = len(replace[0])
131        for line in replace:
132            spaces = 0
133            for i, c in enumerate(line):
134                if c != ' ':
135                    # Non-preprocessor line ==> skip the fixup
136                    if not all_pound and c != '#':
137                        return replace
138                    spaces = i
139                    break
140            min_spaces = min(min_spaces, spaces)
141
142        replace = [line[min_spaces:] for line in replace]
143
144        if all_pound:
145            replace = ["#" + line for line in replace]
146
147        return replace
148
149    def _handle_if_block(self, macro, idx, is_true, prepend):
150        """
151        Remove the #if or #elif block starting on this line.
152        """
153        REMOVE_ONE = 0
154        KEEP_ONE = 1
155        REMOVE_REST = 2
156
157        if is_true:
158            state = KEEP_ONE
159        else:
160            state = REMOVE_ONE
161
162        line = self._inlines[idx]
163        is_if = self._if.match(line) is not None
164        assert is_if or self._elif.match(line) is not None
165        depth = 0
166
167        start_idx = idx
168
169        idx += 1
170        replace = prepend
171        finished = False
172        while idx < len(self._inlines):
173            line = self._inlines[idx]
174            # Nested if statement
175            if self._if.match(line):
176                depth += 1
177                idx += 1
178                continue
179            # We're inside a nested statement
180            if depth > 0:
181                if self._endif.match(line):
182                    depth -= 1
183                idx += 1
184                continue
185
186            # We're at the original depth
187
188            # Looking only for an endif.
189            # We've found a true statement, but haven't
190            # completely elided the if block, so we just
191            # remove the remainder.
192            if state == REMOVE_REST:
193                if self._endif.match(line):
194                    if is_if:
195                        # Remove the endif because we took the first if
196                        idx += 1
197                    finished = True
198                    break
199                idx += 1
200                continue
201
202            if state == KEEP_ONE:
203                m = self._elif.match(line)
204                if self._endif.match(line):
205                    replace += self._inlines[start_idx + 1:idx]
206                    idx += 1
207                    finished = True
208                    break
209                if self._elif.match(line) or self._else.match(line):
210                    replace += self._inlines[start_idx + 1:idx]
211                    state = REMOVE_REST
212                idx += 1
213                continue
214
215            if state == REMOVE_ONE:
216                m = self._elif.match(line)
217                if m is not None:
218                    if is_if:
219                        idx += 1
220                        b = m.start('elif')
221                        e = m.end('elif')
222                        assert e - b == 2
223                        replace.append(line[:b] + line[e:])
224                    finished = True
225                    break
226                m = self._else.match(line)
227                if m is not None:
228                    if is_if:
229                        idx += 1
230                        while self._endif.match(self._inlines[idx]) is None:
231                            replace.append(self._inlines[idx])
232                            idx += 1
233                        idx += 1
234                    finished = True
235                    break
236                if self._endif.match(line):
237                    if is_if:
238                        # Remove the endif because no other elifs
239                        idx += 1
240                    finished = True
241                    break
242                idx += 1
243                continue
244        if not finished:
245            raise RuntimeError("Unterminated if block!")
246
247        replace = self._fixup_indentation(macro, replace)
248
249        self._log(f"\tHardwiring {macro}")
250        if start_idx > 0:
251            self._log(f"\t\t  {self._inlines[start_idx - 1][:-1]}")
252        for x in range(start_idx, idx):
253            self._log(f"\t\t- {self._inlines[x][:-1]}")
254        for line in replace:
255            self._log(f"\t\t+ {line[:-1]}")
256        if idx < len(self._inlines):
257            self._log(f"\t\t  {self._inlines[idx][:-1]}")
258
259        return idx, replace
260
261    def _preprocess_once(self):
262        outlines = []
263        idx = 0
264        changed = False
265        while idx < len(self._inlines):
266            line = self._inlines[idx]
267            sline = self._strip_comments(line)
268            m = self._ifdef.fullmatch(sline)
269            if_true = False
270            if m is None:
271                m = self._if_defined_value.fullmatch(sline)
272            if m is None:
273                m = self._if_defined.match(sline)
274            if m is None:
275                m = self._if_true.match(sline)
276                if_true = (m is not None)
277            if m is None:
278                outlines.append(line)
279                idx += 1
280                continue
281
282            groups = m.groupdict()
283            macro = groups['macro']
284            op = groups.get('op')
285
286            if not (macro in self._defs or macro in self._undefs):
287                outlines.append(line)
288                idx += 1
289                continue
290
291            defined = macro in self._defs
292
293            # Needed variables set:
294            # resolved: Is the statement fully resolved?
295            # is_true: If resolved, is the statement true?
296            ifdef = False
297            if if_true:
298                if not defined:
299                    outlines.append(line)
300                    idx += 1
301                    continue
302
303                defined_value = self._defs[macro]
304                is_int = True
305                try:
306                    defined_value = int(defined_value)
307                except TypeError:
308                    is_int = False
309                except ValueError:
310                    is_int = False
311
312                resolved = is_int
313                is_true = (defined_value != 0)
314
315                if resolved and op is not None:
316                    if op == '&&':
317                        resolved = not is_true
318                    else:
319                        assert op == '||'
320                        resolved = is_true
321
322            else:
323                ifdef = groups.get('not') is None
324                elseif = groups.get('elif') is not None
325
326                macro2 = groups.get('macro2')
327                cmp = groups.get('cmp')
328                value = groups.get('value')
329                openp = groups.get('openp')
330                closep = groups.get('closep')
331
332                is_true = (ifdef == defined)
333                resolved = True
334                if op is not None:
335                    if op == '&&':
336                        resolved = not is_true
337                    else:
338                        assert op == '||'
339                        resolved = is_true
340
341                if macro2 is not None and not resolved:
342                    assert ifdef and defined and op == '&&' and cmp is not None
343                    # If the statement is true, but we have a single value check, then
344                    # check the value.
345                    defined_value = self._defs[macro]
346                    are_ints = True
347                    try:
348                        defined_value = int(defined_value)
349                        value = int(value)
350                    except TypeError:
351                        are_ints = False
352                    except ValueError:
353                        are_ints = False
354                    if (
355                            macro == macro2 and
356                            ((openp is None) == (closep is None)) and
357                            are_ints
358                    ):
359                        resolved = True
360                        if cmp == '<':
361                            is_true = defined_value < value
362                        elif cmp == '<=':
363                            is_true = defined_value <= value
364                        elif cmp == '==':
365                            is_true = defined_value == value
366                        elif cmp == '!=':
367                            is_true = defined_value != value
368                        elif cmp == '>=':
369                            is_true = defined_value >= value
370                        elif cmp == '>':
371                            is_true = defined_value > value
372                        else:
373                            resolved = False
374
375                if op is not None and not resolved:
376                    # Remove the first op in the line + spaces
377                    if op == '&&':
378                        opre = op
379                    else:
380                        assert op == '||'
381                        opre = r'\|\|'
382                    needle = re.compile(fr"(?P<if>\s*#\s*(el)?if\s+).*?(?P<op>{opre}\s*)")
383                    match = needle.match(line)
384                    assert match is not None
385                    newline = line[:match.end('if')] + line[match.end('op'):]
386
387                    self._log(f"\tHardwiring partially resolved {macro}")
388                    self._log(f"\t\t- {line[:-1]}")
389                    self._log(f"\t\t+ {newline[:-1]}")
390
391                    outlines.append(newline)
392                    idx += 1
393                    continue
394
395            # Skip any statements we cannot fully compute
396            if not resolved:
397                outlines.append(line)
398                idx += 1
399                continue
400
401            prepend = []
402            if macro in self._replaces:
403                assert not ifdef
404                assert op is None
405                value = self._replaces.pop(macro)
406                prepend = [f"#define {macro} {value}\n"]
407
408            idx, replace = self._handle_if_block(macro, idx, is_true, prepend)
409            outlines += replace
410            changed = True
411
412        return changed, outlines
413
414    def preprocess(self, filename):
415        with open(filename, 'r') as f:
416            self._inlines = f.readlines()
417        changed = True
418        iters = 0
419        while changed:
420            iters += 1
421            changed, outlines = self._preprocess_once()
422            self._inlines = outlines
423
424        with open(filename, 'w') as f:
425            f.write(''.join(self._inlines))
426
427
428class Freestanding(object):
429    def __init__(
430            self, zstd_deps: str, mem: str, source_lib: str, output_lib: str,
431            external_xxhash: bool, xxh64_state: Optional[str],
432            xxh64_prefix: Optional[str], rewritten_includes: [(str, str)],
433            defs: [(str, Optional[str])], replaces: [(str, str)],
434            undefs: [str], excludes: [str], seds: [str], spdx: bool,
435    ):
436        self._zstd_deps = zstd_deps
437        self._mem = mem
438        self._src_lib = source_lib
439        self._dst_lib = output_lib
440        self._external_xxhash = external_xxhash
441        self._xxh64_state = xxh64_state
442        self._xxh64_prefix = xxh64_prefix
443        self._rewritten_includes = rewritten_includes
444        self._defs = defs
445        self._replaces = replaces
446        self._undefs = undefs
447        self._excludes = excludes
448        self._seds = seds
449        self._spdx = spdx
450
451    def _dst_lib_file_paths(self):
452        """
453        Yields all the file paths in the dst_lib.
454        """
455        for root, dirname, filenames in os.walk(self._dst_lib):
456            for filename in filenames:
457                filepath = os.path.join(root, filename)
458                yield filepath
459
460    def _log(self, *args, **kwargs):
461        print(*args, **kwargs)
462
463    def _copy_file(self, lib_path):
464        suffixes = [".c", ".h", ".S"]
465        if not any((lib_path.endswith(suffix) for suffix in suffixes)):
466            return
467        if lib_path in SKIPPED_FILES:
468            self._log(f"\tSkipping file: {lib_path}")
469            return
470        if self._external_xxhash and lib_path in XXHASH_FILES:
471            self._log(f"\tSkipping xxhash file: {lib_path}")
472            return
473
474        src_path = os.path.join(self._src_lib, lib_path)
475        dst_path = os.path.join(self._dst_lib, lib_path)
476        self._log(f"\tCopying: {src_path} -> {dst_path}")
477        shutil.copyfile(src_path, dst_path)
478
479    def _copy_source_lib(self):
480        self._log("Copying source library into output library")
481
482        assert os.path.exists(self._src_lib)
483        os.makedirs(self._dst_lib, exist_ok=True)
484        self._copy_file("zstd.h")
485        self._copy_file("zstd_errors.h")
486        for subdir in INCLUDED_SUBDIRS:
487            src_dir = os.path.join(self._src_lib, subdir)
488            dst_dir = os.path.join(self._dst_lib, subdir)
489
490            assert os.path.exists(src_dir)
491            os.makedirs(dst_dir, exist_ok=True)
492
493            for filename in os.listdir(src_dir):
494                lib_path = os.path.join(subdir, filename)
495                self._copy_file(lib_path)
496
497    def _copy_zstd_deps(self):
498        dst_zstd_deps = os.path.join(self._dst_lib, "common", "zstd_deps.h")
499        self._log(f"Copying zstd_deps: {self._zstd_deps} -> {dst_zstd_deps}")
500        shutil.copyfile(self._zstd_deps, dst_zstd_deps)
501
502    def _copy_mem(self):
503        dst_mem = os.path.join(self._dst_lib, "common", "mem.h")
504        self._log(f"Copying mem: {self._mem} -> {dst_mem}")
505        shutil.copyfile(self._mem, dst_mem)
506
507    def _hardwire_preprocessor(self, name: str, value: Optional[str] = None, undef=False):
508        """
509        If value=None then hardwire that it is defined, but not what the value is.
510        If undef=True then value must be None.
511        If value='' then the macro is defined to '' exactly.
512        """
513        assert not (undef and value is not None)
514        for filepath in self._dst_lib_file_paths():
515            file = FileLines(filepath)
516
517    def _hardwire_defines(self):
518        self._log("Hardwiring macros")
519        partial_preprocessor = PartialPreprocessor(self._defs, self._replaces, self._undefs)
520        for filepath in self._dst_lib_file_paths():
521            partial_preprocessor.preprocess(filepath)
522
523    def _remove_excludes(self):
524        self._log("Removing excluded sections")
525        for exclude in self._excludes:
526            self._log(f"\tRemoving excluded sections for: {exclude}")
527            begin_re = re.compile(f"BEGIN {exclude}")
528            end_re = re.compile(f"END {exclude}")
529            for filepath in self._dst_lib_file_paths():
530                file = FileLines(filepath)
531                outlines = []
532                skipped = []
533                emit = True
534                for line in file.lines:
535                    if emit and begin_re.search(line) is not None:
536                        assert end_re.search(line) is None
537                        emit = False
538                    if emit:
539                        outlines.append(line)
540                    else:
541                        skipped.append(line)
542                        if end_re.search(line) is not None:
543                            assert begin_re.search(line) is None
544                            self._log(f"\t\tRemoving excluded section: {exclude}")
545                            for s in skipped:
546                                self._log(f"\t\t\t- {s}")
547                            emit = True
548                            skipped = []
549                if not emit:
550                    raise RuntimeError("Excluded section unfinished!")
551                file.lines = outlines
552                file.write()
553
554    def _rewrite_include(self, original, rewritten):
555        self._log(f"\tRewriting include: {original} -> {rewritten}")
556        regex = re.compile(f"\\s*#\\s*include\\s*(?P<include>{original})")
557        for filepath in self._dst_lib_file_paths():
558            file = FileLines(filepath)
559            for i, line in enumerate(file.lines):
560                match = regex.match(line)
561                if match is None:
562                    continue
563                s = match.start('include')
564                e = match.end('include')
565                file.lines[i] = line[:s] + rewritten + line[e:]
566            file.write()
567
568    def _rewrite_includes(self):
569        self._log("Rewriting includes")
570        for original, rewritten in self._rewritten_includes:
571            self._rewrite_include(original, rewritten)
572
573    def _replace_xxh64_prefix(self):
574        if self._xxh64_prefix is None:
575            return
576        self._log(f"Replacing XXH64 prefix with {self._xxh64_prefix}")
577        replacements = []
578        if self._xxh64_state is not None:
579            replacements.append(
580                (re.compile(r"([^\w]|^)(?P<orig>XXH64_state_t)([^\w]|$)"), self._xxh64_state)
581            )
582        if self._xxh64_prefix is not None:
583            replacements.append(
584                (re.compile(r"([^\w]|^)(?P<orig>XXH64)[\(_]"), self._xxh64_prefix)
585            )
586        for filepath in self._dst_lib_file_paths():
587            file = FileLines(filepath)
588            for i, line in enumerate(file.lines):
589                modified = False
590                for regex, replacement in replacements:
591                    match = regex.search(line)
592                    while match is not None:
593                        modified = True
594                        b = match.start('orig')
595                        e = match.end('orig')
596                        line = line[:b] + replacement + line[e:]
597                        match = regex.search(line)
598                if modified:
599                    self._log(f"\t- {file.lines[i][:-1]}")
600                    self._log(f"\t+ {line[:-1]}")
601                file.lines[i] = line
602            file.write()
603
604    def _parse_sed(self, sed):
605        assert sed[0] == 's'
606        delim = sed[1]
607        match = re.fullmatch(f's{delim}(.+){delim}(.*){delim}(.*)', sed)
608        assert match is not None
609        regex = re.compile(match.group(1))
610        format_str = match.group(2)
611        is_global = match.group(3) == 'g'
612        return regex, format_str, is_global
613
614    def _process_sed(self, sed):
615        self._log(f"Processing sed: {sed}")
616        regex, format_str, is_global = self._parse_sed(sed)
617
618        for filepath in self._dst_lib_file_paths():
619            file = FileLines(filepath)
620            for i, line in enumerate(file.lines):
621                modified = False
622                while True:
623                    match = regex.search(line)
624                    if match is None:
625                        break
626                    replacement = format_str.format(match.groups(''), match.groupdict(''))
627                    b = match.start()
628                    e = match.end()
629                    line = line[:b] + replacement + line[e:]
630                    modified = True
631                    if not is_global:
632                        break
633                if modified:
634                    self._log(f"\t- {file.lines[i][:-1]}")
635                    self._log(f"\t+ {line[:-1]}")
636                file.lines[i] = line
637            file.write()
638
639    def _process_seds(self):
640        self._log("Processing seds")
641        for sed in self._seds:
642            self._process_sed(sed)
643
644    def _process_spdx(self):
645        if not self._spdx:
646            return
647        self._log("Processing spdx")
648        SPDX_C = "// SPDX-License-Identifier: GPL-2.0+ OR BSD-3-Clause\n"
649        SPDX_H_S = "/* SPDX-License-Identifier: GPL-2.0+ OR BSD-3-Clause */\n"
650        for filepath in self._dst_lib_file_paths():
651            file = FileLines(filepath)
652            if file.lines[0] == SPDX_C or file.lines[0] == SPDX_H_S:
653                continue
654            for line in file.lines:
655                if "SPDX-License-Identifier" in line:
656                    raise RuntimeError(f"Unexpected SPDX license identifier: {file.filename} {repr(line)}")
657            if file.filename.endswith(".c"):
658                file.lines.insert(0, SPDX_C)
659            elif file.filename.endswith(".h") or file.filename.endswith(".S"):
660                file.lines.insert(0, SPDX_H_S)
661            else:
662                raise RuntimeError(f"Unexpected file extension: {file.filename}")
663            file.write()
664
665
666
667    def go(self):
668        self._copy_source_lib()
669        self._copy_zstd_deps()
670        self._copy_mem()
671        self._hardwire_defines()
672        self._remove_excludes()
673        self._rewrite_includes()
674        self._replace_xxh64_prefix()
675        self._process_seds()
676        self._process_spdx()
677
678
679def parse_optional_pair(defines: [str]) -> [(str, Optional[str])]:
680    output = []
681    for define in defines:
682        parsed = define.split('=')
683        if len(parsed) == 1:
684            output.append((parsed[0], None))
685        elif len(parsed) == 2:
686            output.append((parsed[0], parsed[1]))
687        else:
688            raise RuntimeError(f"Bad define: {define}")
689    return output
690
691
692def parse_pair(rewritten_includes: [str]) -> [(str, str)]:
693    output = []
694    for rewritten_include in rewritten_includes:
695        parsed = rewritten_include.split('=')
696        if len(parsed) == 2:
697            output.append((parsed[0], parsed[1]))
698        else:
699            raise RuntimeError(f"Bad rewritten include: {rewritten_include}")
700    return output
701
702
703
704def main(name, args):
705    parser = argparse.ArgumentParser(prog=name)
706    parser.add_argument("--zstd-deps", default="zstd_deps.h", help="Zstd dependencies file")
707    parser.add_argument("--mem", default="mem.h", help="Memory module")
708    parser.add_argument("--source-lib", default="../../lib", help="Location of the zstd library")
709    parser.add_argument("--output-lib", default="./freestanding_lib", help="Where to output the freestanding zstd library")
710    parser.add_argument("--xxhash", default=None, help="Alternate external xxhash include e.g. --xxhash='<xxhash.h>'. If set xxhash is not included.")
711    parser.add_argument("--xxh64-state", default=None, help="Alternate XXH64 state type (excluding _) e.g. --xxh64-state='struct xxh64_state'")
712    parser.add_argument("--xxh64-prefix", default=None, help="Alternate XXH64 function prefix (excluding _) e.g. --xxh64-prefix=xxh64")
713    parser.add_argument("--rewrite-include", default=[], dest="rewritten_includes", action="append", help="Rewrite an include REGEX=NEW (e.g. '<stddef\\.h>=<linux/types.h>')")
714    parser.add_argument("--sed", default=[], dest="seds", action="append", help="Apply a sed replacement. Format: `s/REGEX/FORMAT/[g]`. REGEX is a Python regex. FORMAT is a Python format string formatted by the regex dict.")
715    parser.add_argument("--spdx", action="store_true", help="Add SPDX License Identifiers")
716    parser.add_argument("-D", "--define", default=[], dest="defs", action="append", help="Pre-define this macro (can be passed multiple times)")
717    parser.add_argument("-U", "--undefine", default=[], dest="undefs", action="append", help="Pre-undefine this macro (can be passed multiple times)")
718    parser.add_argument("-R", "--replace", default=[], dest="replaces", action="append", help="Pre-define this macro and replace the first ifndef block with its definition")
719    parser.add_argument("-E", "--exclude", default=[], dest="excludes", action="append", help="Exclude all lines between 'BEGIN <EXCLUDE>' and 'END <EXCLUDE>'")
720    args = parser.parse_args(args)
721
722    # Always remove threading
723    if "ZSTD_MULTITHREAD" not in args.undefs:
724        args.undefs.append("ZSTD_MULTITHREAD")
725
726    args.defs = parse_optional_pair(args.defs)
727    for name, _ in args.defs:
728        if name in args.undefs:
729            raise RuntimeError(f"{name} is both defined and undefined!")
730
731    # Always set tracing to 0
732    if "ZSTD_NO_TRACE" not in (arg[0] for arg in args.defs):
733        args.defs.append(("ZSTD_NO_TRACE", None))
734        args.defs.append(("ZSTD_TRACE", "0"))
735
736    args.replaces = parse_pair(args.replaces)
737    for name, _ in args.replaces:
738        if name in args.undefs or name in args.defs:
739            raise RuntimeError(f"{name} is both replaced and (un)defined!")
740
741    args.rewritten_includes = parse_pair(args.rewritten_includes)
742
743    external_xxhash = False
744    if args.xxhash is not None:
745        external_xxhash = True
746        args.rewritten_includes.append(('"(\\.\\./common/)?xxhash.h"', args.xxhash))
747
748    if args.xxh64_prefix is not None:
749        if not external_xxhash:
750            raise RuntimeError("--xxh64-prefix may only be used with --xxhash provided")
751
752    if args.xxh64_state is not None:
753        if not external_xxhash:
754            raise RuntimeError("--xxh64-state may only be used with --xxhash provided")
755
756    Freestanding(
757        args.zstd_deps,
758        args.mem,
759        args.source_lib,
760        args.output_lib,
761        external_xxhash,
762        args.xxh64_state,
763        args.xxh64_prefix,
764        args.rewritten_includes,
765        args.defs,
766        args.replaces,
767        args.undefs,
768        args.excludes,
769        args.seds,
770        args.spdx,
771    ).go()
772
773if __name__ == "__main__":
774    main(sys.argv[0], sys.argv[1:])
775