diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..442f72d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,32 @@ +# Repository Guidelines + +## Project Structure & Module Organization +- Core Python modules live at the repo root (e.g., `parser.py`, `ninjabuild.py`, `build.py`, `visitor.py`). +- Shared helpers are in `helpers/` and `helpers.py`. +- Tests live in `test/`, with fixtures under `test/data/`. +- `README.md` documents usage and example workflows. + +## Build, Test, and Development Commands +- `python parser.py -p "." path/to/build.ninja path/to/src` runs the main CLI to translate a Ninja build; see `README.md` for full examples and flags. +- `python -m unittest discover -s test` runs the unit test suite using the standard library. +- `pytest` runs the same tests if you prefer pytest (needed for `test/test_integration_build_files.py`). + +## Coding Style & Naming Conventions +- Use 4-space indentation and PEP 8 style. +- Follow the line-length limit of 110 characters (see `tox.ini`). +- Prefer `snake_case` for functions/variables and `CamelCase` for classes. +- Test files follow `test_*.py` naming and use `unittest.TestCase`. + +## Testing Guidelines +- Add tests for new parsing behaviors and edge cases in `test/`. +- Keep tests deterministic and local; avoid network or system-specific dependencies. +- When adding fixtures, place them under `test/data/` with descriptive names. + +## Commit & Pull Request Guidelines +- Commit messages in this repo use short, imperative sentences without prefixes (e.g., "Add documentation on generated stuff"). +- Keep commits focused on one change set. +- PRs should include a clear summary, testing notes (commands run), and links to relevant issues when applicable. + +## Security & Configuration Tips +- The tool reads local build files and source trees; avoid committing or referencing sensitive paths in fixtures or examples. +- Prefer relative paths in docs and tests so examples work across machines. diff --git a/bazel.py b/bazel.py index 97fda14..126f9ce 100644 --- a/bazel.py +++ b/bazel.py @@ -4,8 +4,19 @@ from copy import deepcopy from functools import cache, cmp_to_key, total_ordering from itertools import combinations -from typing import (Any, Callable, Dict, List, Optional, Set, Type, TypeVar, - Union) +from typing import ( + Any, + Callable, + Dict, + Iterable, + List, + Optional, + Set, + Tuple, + Type, + TypeVar, + Union, +) PREGENERATED_LOCATION = "" @@ -14,6 +25,29 @@ T = TypeVar("T") CompilationFlags = Dict[str, Union[str, Set[str]]] +BASIC_C_STD_RE = re.compile(r"^-std=(?:c|gnu)[0-9A-Za-z]+$") +BASIC_CXX_STD_RE = re.compile(r"^-std=(?:c\+\+|gnu\+\+)[0-9A-Za-z]+$") + + +def _normalize_flag(flag: str) -> str: + return flag.strip('"') + + +def _split_language_opts( + opts: Iterable[str], +) -> Tuple[Set[str], Set[str], Set[str]]: + conlyopts: Set[str] = set() + cxxopts: Set[str] = set() + copts: Set[str] = set() + for opt in opts: + normalized = _normalize_flag(opt) + if BASIC_C_STD_RE.match(normalized): + conlyopts.add(opt) + elif BASIC_CXX_STD_RE.match(normalized) or normalized.startswith("-stdlib="): + cxxopts.add(opt) + else: + copts.add(opt) + return conlyopts, cxxopts, copts def _getPrefix( @@ -416,19 +450,23 @@ def genAdditionalDeps(self): if not (f.name.endswith(".c") or f.name.endswith(".C")) ] if len(c_sources) and len(nonc_sources): + conlyopts, cxxopts, copts = _split_language_opts(t.copts) + t.copts = copts + t.conlyopts.update(conlyopts) + t.cxxopts.update(cxxopts) sublib = BazelTarget(t.type, f"_{t.name}_c", t.location) sublib.includeDirs = deepcopy(t.includeDirs) sublib.srcs = c_sources sublib.addPrefixIfRequired = t.addPrefixIfRequired - sublib.copts = [ - o for o in t.copts if not re.match(r'^"-std=(?:c|gnu)\+\+', o) - ] + sublib.copts = set(t.copts) + sublib.conlyopts = set(t.conlyopts) + sublib.cxxopts = set() sublib.defines = t.defines sublib.deps = t.deps sublib.hdrs = t.hdrs self.bazelTargets.add(sublib) - t.copts = [o for o in t.copts if not re.match(r'^"-std=(?:c|gnu)\d', o)] + t.conlyopts = set() t.srcs = nonc_sources t.hdrs = set() t.deps = set() @@ -510,7 +548,7 @@ def genBazelBuildContent(self) -> Dict[str, str]: # Add some scaffolding for common options that could be easily tweaked vals = [] flags_n_opts = self.commonFlags.get(k, {}) - for c in ["copts", "defines", "linkopts"]: + for c in ["copts", "conlyopts", "cxxopts", "defines", "linkopts"]: flags = flags_n_opts.get(c, set()) if isinstance(flags, str): vals.append(f"common_{c} = {flags}\n") @@ -629,11 +667,16 @@ def __init__(self, type: str, name: str, location: str): self.includeDirs: set[IncludeDir] = set() self.addPrefixIfRequired: bool = True self.copts: set[str] = set() + self.conlyopts: set[str] = set() + self.cxxopts: set[str] = set() self.defines: set[str] = set() self.data: set[BaseBazelTarget] = set() def addCopt(self, opt: str): - self.copts.add(opt) + conlyopts, cxxopts, copts = _split_language_opts([opt]) + self.conlyopts.update(conlyopts) + self.cxxopts.update(cxxopts) + self.copts.update(copts) def addDefine(self, define: str): self.defines.add(define) @@ -738,7 +781,13 @@ def asBazel( sources = [f for f in self.srcs] includes = set() copts = set() + conlyopts, cxxopts, copts = _split_language_opts(self.copts) + self.copts = copts + self.conlyopts.update(conlyopts) + self.cxxopts.update(cxxopts) copts.update(self.copts) + conlyopts = set(self.conlyopts) + cxxopts = set(self.cxxopts) for dir in list(self.includeDirs): includes.add(f'"{dir[0]}"') # FIXME for the moment move defines to copts so that they are not propagated to @@ -757,6 +806,8 @@ def asBazel( "srcs": sources, "hdrs": headers, "copts": list(copts), + "conlyopts": list(conlyopts), + "cxxopts": list(cxxopts), "defines": list(self.defines), "data": data, "includes": list(includes), diff --git a/test/test_bazel.py b/test/test_bazel.py index fb4515f..78851cf 100644 --- a/test/test_bazel.py +++ b/test/test_bazel.py @@ -123,8 +123,10 @@ def test_gen_additional_deps_splits_c_sources(self) -> None: self.assertIn(sublib, target.deps) self.assertNotIn(c_src, target.srcs) self.assertIn(cpp_src, target.srcs) - self.assertIn('"-std=c11"', sublib.copts) + self.assertIn('"-std=c11"', sublib.conlyopts) self.assertNotIn('"-std=c11"', target.copts) + self.assertNotIn('"-std=c11"', target.conlyopts) + self.assertIn('"-O2"', sublib.copts) def test_gen_bazel_build_content_includes_various_targets(self) -> None: build = BazelBuild("src/")