#!/usr/bin/env python3 import argparse import ast import fcntl import functools import hashlib import json import os import re import shlex import shutil import subprocess import sys import tempfile import time from pathlib import Path IGNORED_DIRECTORIES = { ".direnv", ".git", ".hg", ".mypy_cache", ".pixi", ".pytest_cache", ".ruff_cache", ".svn", ".tox", ".venv", "__pycache__", "build", "dist", "node_modules", "target", "test", "tests", } PYTHON_SUFFIXES = {".py", ".pyi"} RUST_SUFFIXES = {".rs"} GO_SUFFIXES = {".go", ".templ"} TEMPL_CTAGS_OPTIONS = [ "--map-Go=+.templ", r"--regex-Go=/^[[:space:]]*(templ|css|script)[[:space:]]+([[:alpha:]_][[:alnum:]_]*)[[:space:]]*\(/\2/f/", ] C_SOURCE_SUFFIXES = {".c", ".cc", ".cpp", ".cxx", ".m", ".mm"} C_HEADER_SUFFIXES = {".h", ".hh", ".hpp", ".hxx"} C3_SUFFIXES = {".c3", ".c3i", ".c3t"} C3_SKIP = r'(?s:/\*.*?\*/|<\*.*?\*>|`[^`]*`|"(?:\\.|[^"\\])*"|//[^\n]*)(*SKIP)(*F)|(?m)^\h*' C3_CTAGS_OPTIONS = [ "--langdef=C3", "--map-C3=+.c3.c3i.c3t", "--kinddef-C3=f,function,functions", "--kinddef-C3=m,macro,macros", "--kinddef-C3=t,type,types", "--kinddef-C3=c,constant,constants", "--kinddef-C3=v,variable,variables", "--kinddef-C3=n,module,modules", *[ rf"--mline-regex-C3=~{C3_SKIP}{pattern}~\1~{kind}~p{{mgroup=1}}" for pattern, kind in [ (r"fn\s+[^;{}()]*?(@?[a-z_]\w*)\s*\(", "f"), (r"macro\s+[^;{}()]*?(@?[a-z_]\w*)\s*\(", "m"), (r"(?:struct|union|enum|bitstruct|interface|def|typedef|distinct|alias|faultdef|attrdef)\s+(@?[A-Za-z_]\w*)", "t"), (r"const\s+[^=;{}]*?\b([A-Z_][A-Z_0-9]*)\s*(?=[=;@])", "c"), (r"module\s+([a-z_]\w*(?:::[a-z_]\w*)*)", "n"), (r"(?:static\s+|tlocal\s+)?(?:(?:[a-z_]\w*::)*[A-Z]\w*|bool|char|ichar|short|ushort|int|uint|long|ulong|int128|uint128|float|double|float16|float128|usz|isz|uptr|iptr|any|typeid)(?:[*!?]|\[[^\]\n]*\])*\s+([a-z_]\w*)\s*(?=[=;@])", "v"), ] ], ] NIX_CTAGS_OPTIONS = [ "--langdef=nix", "--map-nix=+.nix", "--kinddef-nix=a,attribute,attributes", "--regex-nix=/^[[:space:]]*([A-Za-z_][A-Za-z0-9_'-]*(\\.[A-Za-z_][A-Za-z0-9_'-]+)+)[[:space:]]*=[^=]/\\1/a/", "--regex-nix=/^[[:space:]]*([A-Za-z_][A-Za-z0-9_'-]*)[[:space:]]*=[^=]/\\1/a/", ] PYTHON_RESOLVER = """ import importlib.util import json import pathlib import sysconfig import sys resolved = {} stdlib = pathlib.Path(sysconfig.get_path("stdlib")) for name in sys.argv[1:]: try: spec = importlib.util.find_spec(name) except (ImportError, ModuleNotFoundError, ValueError): spec = None paths = [] if spec is not None and spec.submodule_search_locations: paths.extend(spec.submodule_search_locations) elif spec is not None and spec.origin not in (None, "built-in", "frozen"): paths.append(spec.origin) elif spec is not None and spec.origin == "frozen": module_file = stdlib / (name + ".py") package_file = stdlib / name / "__init__.py" if module_file.is_file(): paths.append(str(module_file)) elif package_file.is_file(): paths.append(str(package_file.parent)) resolved[name] = paths if spec is not None else None print(json.dumps(resolved)) """ def parse_arguments(): parser = argparse.ArgumentParser( description="Generate one Citre-compatible .tags file for project code and used dependencies." ) parser.add_argument("root", nargs="?", default=".") parser.add_argument("--output", default=".tags") parser.add_argument("--ctags") parser.add_argument("--python") parser.add_argument("--python-command") parser.add_argument("--cargo") parser.add_argument("--go") parser.add_argument("--c3c") parser.add_argument("--c3-stdlib") parser.add_argument("--source", action="append", default=[]) parser.add_argument("--no-python-deps", action="store_true") parser.add_argument("--no-rust-deps", action="store_true") parser.add_argument("--no-go-deps", action="store_true") parser.add_argument("--no-c-deps", action="store_true") parser.add_argument("--no-c3-deps", action="store_true") parser.add_argument("--cache-dir", help="Maintain project and dependency indexes here") parser.add_argument("--force", action="store_true") parser.add_argument("--snapshot-request", help="JSON list of source/snapshot/output mappings") parser.add_argument("--exclude-dir", action="append", default=[]) parser.add_argument("--watch-limit", type=int, default=256) parser.add_argument("--dependency-check-interval", type=float, default=300) return parser.parse_args() def resolved_path(value, base): expanded = os.path.expandvars(os.path.expanduser(value)) path = Path(expanded) if not path.is_absolute(): path = base / path return path.resolve() def walk_files(path, suffixes=None): if path.is_file(): if suffixes is None or path.suffix.lower() in suffixes: return [path.resolve()] return [] if not path.is_dir(): return [] files = [] for directory, names, filenames in os.walk(path): names[:] = [name for name in names if name not in IGNORED_DIRECTORIES] parent = Path(directory) for filename in filenames: candidate = parent / filename if suffixes is None or candidate.suffix.lower() in suffixes: files.append(candidate.resolve()) return files def git_project_files(root): git = shutil.which("git") if git is None: return None result = subprocess.run( [git, "-C", str(root), "ls-files", "--cached", "--others", "--exclude-standard", "-z"], stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=False, ) if result.returncode != 0: return None files = [] for name in result.stdout.split(b"\0"): if not name: continue path = root / os.fsdecode(name) if path.is_file(): files.append(path.resolve()) return files def project_files(root, output): files = git_project_files(root) if files is None: files = walk_files(root) return sorted(path for path in files if path != output) def imported_modules(files): modules = set() for path in files: if path.suffix.lower() not in PYTHON_SUFFIXES: continue try: tree = ast.parse(path.read_bytes(), filename=str(path)) except (OSError, SyntaxError, UnicodeError): continue for node in ast.walk(tree): if isinstance(node, ast.Import): modules.update(alias.name.partition(".")[0] for alias in node.names) elif isinstance(node, ast.ImportFrom) and node.level == 0 and node.module: modules.add(node.module.partition(".")[0]) return sorted(modules) def python_command(args, root): if args.python_command: return shlex.split(args.python_command) environment_command = os.environ.get("PROJECT_CTAGS_PYTHON_COMMAND") if environment_command: return shlex.split(environment_command) if args.python: path = Path(os.path.expandvars(os.path.expanduser(args.python))) if not path.is_absolute(): path = root / path return [str(path.absolute())] virtual_environment = os.environ.get("VIRTUAL_ENV") candidates = [] if virtual_environment: candidates.append(Path(virtual_environment) / "bin" / "python") candidates.extend( [ root / ".venv" / "bin" / "python", root / ".devenv" / "state" / "venv" / "bin" / "python", root / ".pixi" / "envs" / "default" / "bin" / "python", ] ) for candidate in candidates: if candidate.is_file() and os.access(candidate, os.X_OK): return [str(candidate.absolute())] uv = shutil.which("uv") if uv and (root / "uv.lock").exists(): return [uv, "run", "--no-sync", "python"] python = shutil.which("python3") or shutil.which("python") if python: return [python] return None def python_dependency_files(args, root, files): modules = imported_modules(files) if not modules: return [], None, [] command = python_command(args, root) if command is None: return [], None, modules result = subprocess.run( command + ["-c", PYTHON_RESOLVER, *modules], cwd=root, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, ) if result.returncode != 0: print(result.stderr.rstrip(), file=sys.stderr) return [], shlex.join(command), modules try: resolved = json.loads(result.stdout) except json.JSONDecodeError: print(f"Could not read module paths from {shlex.join(command)}", file=sys.stderr) return [], shlex.join(command), modules dependency_files = set() unresolved = [] for module, paths in resolved.items(): if paths is None: unresolved.append(module) continue module_files = [] for value in paths: path = Path(value).resolve() module_files.extend(walk_files(path, PYTHON_SUFFIXES)) dependency_files.update(module_files) return sorted(dependency_files), shlex.join(command), unresolved def cargo_command(args, root): value = args.cargo or os.environ.get("PROJECT_CTAGS_CARGO") if value: path = Path(os.path.expandvars(os.path.expanduser(value))) if not path.is_absolute(): path = root / path return str(path.absolute()) return shutil.which("cargo") def rust_host_target(root): rustc = shutil.which("rustc") if rustc is None: return None result = subprocess.run( [rustc, "-vV"], cwd=root, text=True, stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, check=False, ) if result.returncode == 0: for line in result.stdout.splitlines(): if line.startswith("host: "): return line.removeprefix("host: ") return None def rust_dependency_files(args, root): manifest = root / "Cargo.toml" if not manifest.is_file(): return [], None cargo = cargo_command(args, root) if cargo is None: return [], None command = [ cargo, "metadata", "--format-version=1", "--locked", "--offline", "--manifest-path", str(manifest), ] target = rust_host_target(root) if target: command.extend(["--filter-platform", target]) result = subprocess.run( command, cwd=root, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, ) if result.returncode != 0: if result.stderr: print(result.stderr.rstrip(), file=sys.stderr) return [], None try: metadata = json.loads(result.stdout) except json.JSONDecodeError: print("Could not read Cargo metadata", file=sys.stderr) return [], None workspace_members = set(metadata.get("workspace_members", [])) files = set() for package in metadata.get("packages", []): if package.get("id") in workspace_members: continue package_root = Path(package["manifest_path"]).resolve().parent files.update(walk_files(package_root, RUST_SUFFIXES)) return sorted(files), cargo def go_dependency_files(args, root, files): """Resolve imported Go packages, including the standard library, from local sources.""" if args.no_go_deps or not any(path.suffix.lower() in GO_SUFFIXES for path in files): return [], None value = args.go or os.environ.get("PROJECT_CTAGS_GO") or shutil.which("go") if not value: print("Go dependencies skipped: go is unavailable in the project environment", file=sys.stderr) return [], None go = shutil.which(value) or str(resolved_path(value, root)) environment = dict(os.environ, GOPROXY="off", GOSUMDB="off", GOTOOLCHAIN="local") result = subprocess.run( [go, "list", "-e", "-deps", "-json", "-mod=readonly", "./..."], cwd=root, env=environment, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, ) if result.stderr.strip(): print(result.stderr.rstrip(), file=sys.stderr) dependencies = set() decoder = json.JSONDecoder() remaining = result.stdout.lstrip() while remaining: try: package, end = decoder.raw_decode(remaining) except json.JSONDecodeError: print("Could not read Go package metadata", file=sys.stderr) break remaining = remaining[end:].lstrip() if error := package.get("Error"): print(f"go: {error.get('Err', error)}", file=sys.stderr) if not package.get("Dir"): continue directory = Path(package["Dir"]).resolve() if directory.is_relative_to(root): continue for name in package.get("GoFiles", []) + package.get("CgoFiles", []): path = directory / name if path.is_file(): dependencies.add(path.resolve()) return sorted(dependencies), go def compilation_database(root): path = root / "compile_commands.json" if not path.is_file(): return {} try: entries = json.loads(path.read_text()) except (OSError, json.JSONDecodeError): return {} database = {} for entry in entries: directory = resolved_path(entry.get("directory", root), root) source = resolved_path(entry["file"], directory) arguments = entry.get("arguments") if arguments is None and entry.get("command"): arguments = shlex.split(entry["command"]) if arguments: database[source] = (directory, arguments) return database def dependency_command(source, root, database): entry = database.get(source) if entry is None: compiler = shlex.split(os.environ.get("CC", "cc")) flags = shlex.split(os.environ.get("CPPFLAGS", "")) flags.extend(shlex.split(os.environ.get("CFLAGS", ""))) return root, compiler + ["-M", "-MG", *flags, str(source)] directory, arguments = entry compiler = arguments[0] flags = [] skip_next = False options_with_values = {"-MF", "-MT", "-MQ", "-o"} for argument in arguments[1:]: if skip_next: skip_next = False continue if argument in options_with_values: skip_next = True continue if argument == "-c" or argument.startswith("-M"): continue candidate = resolved_path(argument, directory) if candidate == source: continue flags.append(argument) return directory, [compiler, "-M", "-MG", *flags, str(source)] def make_dependencies(output, directory): flattened = output.replace("\\\n", " ") if ":" not in flattened: return [] dependencies = flattened.split(":", 1)[1] paths = [] for value in shlex.split(dependencies): path = resolved_path(value, directory) if path.is_file() and path.suffix.lower() in C_HEADER_SUFFIXES: paths.append(path) return paths def c_dependency_files(root, files): sources = [path for path in files if path.suffix.lower() in C_SOURCE_SUFFIXES] if not sources: return [] database = compilation_database(root) headers = set() for source in sources: directory, command = dependency_command(source, root, database) result = subprocess.run( command, cwd=directory, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE, check=False, ) if result.returncode == 0: headers.update(make_dependencies(result.stdout, directory)) elif result.stderr: print(result.stderr.rstrip(), file=sys.stderr) return sorted(headers) def extra_source_files(args, root): values = list(args.source) environment_sources = os.environ.get("PROJECT_CTAGS_SOURCES") if environment_sources: values.extend(value for value in environment_sources.split(os.pathsep) if value) files = set() for value in values: path = resolved_path(value, root) if not path.exists(): print(f"Extra source does not exist: {path}", file=sys.stderr) continue files.update(walk_files(path)) return sorted(files) @functools.lru_cache(maxsize=4) def c3_ctags_options(ctags): result = subprocess.run( [ctags, "--options=NONE", "--list-languages"], text=True, capture_output=True, check=True, ) return [] if "C3" in result.stdout.splitlines() else C3_CTAGS_OPTIONS def c3_dependency_files(args, root, files): """Include the installed C3 standard library when indexing a C3 project.""" if args.no_c3_deps or not any( path.suffix.lower() in C3_SUFFIXES for path in files ): return [] library = args.c3_stdlib or os.environ.get("C3_STDLIB") if library: return walk_files(resolved_path(library, root), C3_SUFFIXES) compiler = args.c3c or os.environ.get("PROJECT_CTAGS_C3C") or shutil.which("c3c") if not compiler: return [] executable = shutil.which(compiler) or compiler prefix = resolved_path(executable, root).parent.parent for candidate in ( prefix / "lib" / "c3", prefix / "bin" / "lib", prefix / "lib", ): if (candidate / "std").is_dir(): return walk_files(candidate, C3_SUFFIXES) return [] def generate_tags(ctags, output, files, mappings=None): output.parent.mkdir(parents=True, exist_ok=True) list_file = tempfile.NamedTemporaryFile(mode="w", prefix="project-ctags-", delete=False) temporary_output = tempfile.NamedTemporaryFile( prefix=f".{output.name}.", dir=output.parent, delete=False ) temporary_output.close() try: for path in files: if "\n" in str(path) or "\r" in str(path): raise ValueError(f"Ctags file lists cannot represent this path: {path!r}") list_file.write(f"{path}\n") list_file.close() command = [ ctags, "--options=NONE", "--output-format=e-ctags", *NIX_CTAGS_OPTIONS, *TEMPL_CTAGS_OPTIONS, *c3_ctags_options(ctags), "--languages=all", "--kinds-all=*", "--fields=*", "--extras=*", "-f", temporary_output.name, "-L", list_file.name, ] result = subprocess.run(command, check=False) if result.returncode != 0: raise SystemExit(result.returncode) if mappings: data = Path(temporary_output.name).read_bytes() for source, original in mappings.items(): data = data.replace(b"\t" + tag_path(source) + b"\t", b"\t" + tag_path(original) + b"\t") Path(temporary_output.name).write_bytes(data) os.replace(temporary_output.name, output) finally: list_file.close() Path(list_file.name).unlink(missing_ok=True) Path(temporary_output.name).unlink(missing_ok=True) def tag_path(path): """Encode filenames in the tags input field.""" return os.fsencode(path).replace(b"\\", b"\\\\").replace(b"\t", b"\\t").replace(b"\n", b"\\n").replace(b"\r", b"\\r") def file_stamp(path): try: value = Path(path).stat() return [value.st_mtime_ns, value.st_ctime_ns, value.st_size] except OSError: return None def atomic_json(path, value): with tempfile.NamedTemporaryFile(mode="w", dir=path.parent, delete=False) as stream: temporary = Path(stream.name) json.dump(value, stream) try: os.replace(temporary, path) finally: temporary.unlink(missing_ok=True) def incremental_tags(ctags, output, current, previous, force=False): """Parse changed files and replace their old records, including empty/deleted files.""" changed = {name for name, stamp in current.items() if previous.get(name) != stamp} removed = previous.keys() - current.keys() if force or not output.exists(): generate_tags(ctags, output, [Path(name) for name in current]) return True if not changed and not removed: return False with tempfile.TemporaryDirectory(prefix="update-", dir=output.parent) as temporary: delta = Path(temporary) / "delta.tags" generate_tags(ctags, delta, [Path(name) for name in sorted(changed)]) replaced = {tag_path(name) for name in changed | removed} headers = {} records = [] for source, old in ((output, True), (delta, False)): for line in source.read_bytes().splitlines(keepends=True): if line.startswith(b"!_TAG_"): headers[line.split(b"\t", 1)[0]] = line elif line.strip(): fields = line.split(b"\t", 2) if not old or (len(fields) > 1 and fields[1] not in replaced): records.append(line) merged = Path(temporary) / "merged.tags" merged.write_bytes(b"".join(headers.values()) + b"".join(sorted(set(records)))) os.replace(merged, output) return True def dependency_signature(args, root, files): """Track declarations without resolving dependencies on every source edit.""" declarations = [] for path in files: if path.name in { "Cargo.toml", "Cargo.lock", "project.json", "pyproject.toml", "uv.lock", "poetry.lock", "pixi.lock", "pixi.toml", "requirements.txt", "compile_commands.json", "flake.lock", "flake.nix", ".envrc", "go.mod", "go.sum", "go.work", "go.work.sum", }: declarations.append((str(path), file_stamp(path))) elif path.suffix.lower() in GO_SUFFIXES: try: imports = re.findall(r'(?m)^\s*import\s*(?:\([^)]*\)|[^\n]+)', path.read_text(errors="replace")) declarations.append((str(path), imports)) except OSError: pass elif path.suffix.lower() in C_SOURCE_SUFFIXES | C_HEADER_SUFFIXES: try: includes = [line.strip() for line in path.read_text(errors="replace").splitlines() if line.lstrip().startswith(("#include", "# include"))] declarations.append((str(path), includes)) except OSError: pass value = [imported_modules(files), declarations, {key: item for key, item in vars(args).items() if key != "force"}, { key: os.environ.get(key) for key in ( "PATH", "VIRTUAL_ENV", "CC", "CFLAGS", "CPPFLAGS", "PROJECT_CTAGS_PYTHON_COMMAND", "PROJECT_CTAGS_CARGO", "PROJECT_CTAGS_C3C", "C3_STDLIB", "PROJECT_CTAGS_SOURCES", "PROJECT_CTAGS_GO", "GOROOT", "GOPATH", "GOMODCACHE", "GOWORK", "GOOS", "GOARCH", "CGO_ENABLED", "GOFLAGS", ) }, [(str(root / name), file_stamp(root / name)) for name in (".venv", ".pixi/envs/default", ".devenv/state/venv")]] return hashlib.sha256(json.dumps(value, sort_keys=True).encode()).hexdigest() def refresh_cache(args, root, ctags): cache = resolved_path(args.cache_dir, root) cache.mkdir(parents=True, exist_ok=True) with (cache / "lock").open("w") as lock: fcntl.flock(lock, fcntl.LOCK_EX) manifest_path = cache / "manifest.json" try: old = json.loads(manifest_path.read_text()) except (OSError, ValueError): old = {} identity = [3, str(root), str(ctags), file_stamp(ctags), file_stamp(__file__), args.exclude_dir] force = args.force or old.get("identity") != identity project_output = cache / "project.tags" dependency_output = cache / "dependencies.tags" excluded = set(args.exclude_dir) files = [path for path in project_files(root, resolved_path(args.output, root)) if not path.is_relative_to(cache) and not any(part in excluded for part in path.parts)] current = {str(path): file_stamp(path) for path in files} project_changed = incremental_tags(ctags, project_output, current, old.get("files", {}), force or old.get("project_stamp") != file_stamp(project_output)) print(json.dumps({"event": "project", "changed": project_changed}), flush=True) signature = dependency_signature(args, root, files) resolve = force or old.get("dependency_signature") != signature or time.time() - old.get("dependency_checked", 0) >= args.dependency_check_interval dependencies = old.get("dependencies", {}) checked = old.get("dependency_checked", 0) if resolve: dependency_files = [] if not args.no_python_deps: dependency_files.extend(python_dependency_files(args, root, files)[0]) if not args.no_rust_deps: dependency_files.extend(rust_dependency_files(args, root)[0]) dependency_files.extend(go_dependency_files(args, root, files)[0]) if not args.no_c_deps: dependency_files.extend(c_dependency_files(root, files)) dependency_files.extend(extra_source_files(args, root)) dependency_files.extend(c3_dependency_files(args, root, files)) dependencies = {str(path): file_stamp(path) for path in sorted(set(dependency_files)) if str(path) not in current} checked = time.time() else: dependencies = {name: stamp for name in dependencies if (stamp := file_stamp(name)) is not None} dependency_changed = incremental_tags(ctags, dependency_output, dependencies, old.get("dependencies", {}), force or old.get("dependency_stamp") != file_stamp(dependency_output)) manifest = {"identity": identity, "files": current, "dependencies": dependencies, "dependency_signature": signature, "dependency_checked": checked, "project_stamp": file_stamp(project_output), "dependency_stamp": file_stamp(dependency_output), "directories": sorted({str(path.parent) for path in files} | {str(root)}), "checked": time.time()} atomic_json(manifest_path, manifest) print(json.dumps({"event": "complete", "changed": project_changed or dependency_changed, "project_files": len(files), "dependency_files": len(dependencies), "directories": manifest["directories"][:max(0, args.watch_limit)]}), flush=True) def snapshot_tags(args, ctags): for entry in json.loads(Path(args.snapshot_request).read_text()): source = Path(entry["snapshot"]) generate_tags(ctags, Path(entry["output"]), [source], {source: Path(entry["source"])}) def main(): args = parse_arguments() root = resolved_path(args.root, Path.cwd()) if not root.is_dir(): raise SystemExit(f"Project root does not exist: {root}") output = resolved_path(args.output, root) ctags = args.ctags or shutil.which("ctags") if ctags is None: raise SystemExit("Universal Ctags is not available") if args.snapshot_request: snapshot_tags(args, ctags) return if args.cache_dir: refresh_cache(args, root, ctags) return local_files = project_files(root, output) python_files = [] python = None unresolved = [] if not args.no_python_deps: python_files, python, unresolved = python_dependency_files(args, root, local_files) rust_files = [] cargo = None if not args.no_rust_deps: rust_files, cargo = rust_dependency_files(args, root) c_headers = [] if not args.no_c_deps: c_headers = c_dependency_files(root, local_files) extra_files = extra_source_files(args, root) c3_files = c3_dependency_files(args, root, local_files) go_files, go = go_dependency_files(args, root, local_files) all_files = sorted( set(local_files + python_files + rust_files + go_files + c_headers + c3_files + extra_files) ) if not all_files: raise SystemExit("No source files found") generate_tags(ctags, output, all_files) print(f"project: {len(local_files)} files") if python: print(f"python: {python}; {len(python_files)} dependency files") if unresolved: print(f"python unresolved: {', '.join(unresolved)}") if cargo: print(f"rust: {cargo}; {len(rust_files)} dependency files") if c_headers: print(f"c: {len(c_headers)} dependency headers") if go: print(f"go: {go}; {len(go_files)} dependency files") if c3_files: print(f"c3: {len(c3_files)} standard-library files") if extra_files: print(f"extra: {len(extra_files)} files") print(f"tags: {output}") if __name__ == "__main__": main()