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