Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: CI

on:
push:
branches: [main]
pull_request:

permissions:
contents: read

jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: bazel-contrib/setup-bazel@8cb04a772ab4c1eb984e9c1b493a182e96c5e425 # 0.19.0
with:
bazelisk-cache: true
disk-cache: ${{ github.workflow }}
repository-cache: true
- name: Unit tests
run: bazel test //tests:all --test_output=errors
- name: Starlark and generated target analysis
run: bazel build //:all //cc:all //format_all:all //lint_all:all
- name: Stardoc
if: runner.os == 'Linux'
run: bazel build //docs:all
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Ignore macOS folder attributes.
.DS_Store

# Ignore Python bytecode caches produced by running the tests directly.
__pycache__/

# Ignore links to Bazel's output. The pattern needs the `*` because people can change the name of the directory into which the repository is cloned (changing the `bazel-<workspace_name>` symlink), and must not end with a trailing `/` because it's a symlink on macOS/Linux.
/bazel-*
38 changes: 9 additions & 29 deletions MODULE.bazel.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

125 changes: 122 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# rules_devtools

Bazel module for C/C++ and Bazel developer tools: formatting, linting, static analysis, code navigation, and BUILD file tooling. One `use_extension` call wires up everything.
Bazel module for C/C++ and Bazel developer tools: formatting, linting, static analysis, code navigation, Visual Studio solution generation, CMake export, and BUILD file tooling. One `use_extension` call wires up everything.

## Usage

Expand All @@ -9,11 +9,11 @@ Add to your `MODULE.bazel`, pinning to a commit from GitHub. Grab the latest SHA

```starlark
# MODULE.bazel
bazel_dep(name = "rules_devtools", version = "1.0.0")
bazel_dep(name = "rules_devtools", version = "1.9.0")
git_override(
module_name = "rules_devtools",
remote = "https://github.com/onurpaca/rules_devtools.git",
commit = "00802668aafdd1959d229a219788416c2c4155a5",
commit = "<commit-sha>",
)

devtools = use_extension("@rules_devtools//extension:devtools_ext.bzl", "devtools_extension")
Expand Down Expand Up @@ -58,6 +58,8 @@ Run any target with `bazel run @devtools//:TARGET_NAME -- [args]`. All extra arg
| `unused_deps` | Find stale BUILD deps | — | — |
| `sast` | Multi-analyzer: cppcheck + semgrep | — | — |
| `ctags` | Generate code index for navigation | — | — |
| `vs_solution` | Generate a Visual Studio `.sln` + `.vcxproj` | — | — |
| `cmake` | Generate a standalone `CMakeLists.txt` | — | — |

### Bazel (`//bazel:*`)

Expand Down Expand Up @@ -88,8 +90,124 @@ bazel run @devtools//:clang_format -- --check

# Generate compile_commands.json for clangd
bazel run @devtools//:compile_commands

# Generate a Visual Studio solution (opens in full Visual Studio, not VS Code)
bazel run @devtools//:vs_solution

# Generate a standalone CMakeLists.txt from the Bazel cc_* targets
bazel run @devtools//:cmake
```

## Visual Studio

`vs_solution` generates a Visual Studio solution for opening a Bazel workspace in
**full Visual Studio** (not VS Code):

```sh
bazel run @devtools//:vs_solution
```

It scans the configured `targets` for `cc_binary`, `cc_library`, and `cc_test` rules
and writes, at the workspace root, a `<solution_name>.sln` plus one
`.vcxproj` + `.vcxproj.filters` per target under `vs_projects/`.

The projects are **NMake/Makefile** style, so Bazel stays the single source of truth:

- **Build** — `Build`/`Rebuild`/`Clean` shell out to `bazel build`/`bazel clean`
(run from the workspace root). `Debug` maps to `-c dbg`, `Release` to `-c opt`.
- **IntelliSense** — include search paths, preprocessor definitions, forced includes
(`/FI`), and compiler options are taken from `compile_commands.json`. Build-only
flags (optimization, output paths, etc.) are stripped; everything that affects
IntelliSense (`/std`, `/EHsc`, `/W4`, `/wd####`, `/MD`, `/Zc:*`, …) is forwarded.
The sibling `compile_commands` target is **run first automatically** so the data is
always current (disable with `refresh_compile_commands = False` on the macro).
- **Debugging** — `cc_binary` and `cc_test` targets get an `NMakeOutput` /
`LocalDebuggerCommand` pointing at the `bazel-bin` executable, so <kbd>F5</kbd>
launches the built binary.
- **External dependencies** — each target's direct external-dependency headers
(e.g. googletest) are listed under an `External Dependencies\<repo>` filter in
Solution Explorer for browsing.

Generated files (`*.sln`, `vs_projects/`, `.vs/`, `*.vcxproj.user`) are build output —
add them to `.gitignore`.

Set the solution file name via the extension:

```starlark
devtools.configure(
targets = "//...",
vs_solution_name = "myproject", # -> myproject.sln (default: "workspace")
)
```

## CMake export

`cmake` generates a **standalone `CMakeLists.txt`** from a Bazel workspace's `cc_*`
targets — for Bazel-native projects whose users or community consume them via CMake
(the way googletest ships both a `BUILD.bazel` and a hand-maintained `CMakeLists.txt`):

```sh
bazel run @devtools//:cmake # writes CMakeLists.txt to the workspace root
```

It reads the targets' build attributes (`bazel query --output=xml`) and translates the
subset of Bazel that maps cleanly to CMake:

- `cc_library` → `add_library` (STATIC, or INTERFACE when header-only);
`cc_binary` / `cc_test` → `add_executable`.
- `strip_include_prefix` / `includes` → `target_include_directories`.
- `copts` → `target_compile_options`, **guarded by compiler** so GCC/Clang (`-…`) and
MSVC (`/…`) flags coexist correctly. `defines` → `target_compile_definitions`.
- Intra-repo `deps` → `target_link_libraries`; `linkopts` → linked libraries. Implicit
Bazel toolchain deps (e.g. `link_extra_lib`) are dropped.
- Curated external deps (currently googletest) → a `find_package`-or-`FetchContent`
block; `cc_test` → CTest registration with `gtest_discover_tests` when applicable.

**Honest by design — no silent gaps.** Bazel and CMake do not map one-to-one (this is
why projects like googletest hand-maintain both). Anything that cannot be translated
faithfully is emitted as an explicit `# TODO(bazel2cmake): …` comment *and* reported on
the console, so the output is a correct **starting point a maintainer finishes**, not a
guaranteed build. Known limits:

- **`genrule` and generated headers** are not translated (a genrule's command is an
arbitrary script) — flagged with a TODO.
- **`select()` is resolved to the current platform** by `--output=xml`, so
platform-specific `srcs`/`copts`/`linkopts` reflect the build host; other branches are
not emitted.
- **External deps without a curated mapping** are flagged rather than guessed.

## Compilation database

`compile_commands` writes a database that clang-based tools can consume as it is
emitted -- no post-processing wrapper needed:

- **Absolute paths.** Every `-I`/`-isystem`/`-iquote` and source path is resolved
against the execution root, output base, or workspace.
- **Toolchain system headers.** A Bazel compile action never spells out the
toolchain's builtin include directories; the driver knows them implicitly.
clang-tidy and clangd replay the command with *their* driver, so against a
hermetic GCC toolchain they would miss the standard library entirely -- and a
missing `<format>` does not just fail, it makes error recovery invent cascading
findings that look like real defects. Each distinct driver invocation is
therefore probed once (`-x <lang> -E -v` on an empty translation unit under the
action's own `--sysroot`/`-std`/`-nostdinc*`/`--target` flags) and the resulting
search list is written into the entry as explicit `-isystem` flags. Probing the
actual driver -- rather than guessing a layout -- keeps this correct across
toolchain upgrades and across GCC, Clang, and cross-compilation.
- **Compiler-portable flags.** GCC-only driver flags that clang rejects outright
(`-fno-canonical-system-headers`) are dropped, and `-Wno-unknown-warning-option`
is appended so GCC-only warning flags plus `-Werror` cannot turn into clang
errors. On MSVC, `cl.exe` is swapped for `clang-cl` and the MSVC/Windows SDK
includes are added as `-imsvc`.

System-header injection is controlled by the `RULES_DEVTOOLS_SYSTEM_INCLUDES`
environment variable: `auto` (default) injects the standard library and sysroot
directories, `all` also injects the compiler-internal resource directories
(`lib/gcc/<triple>/<ver>/include`, `lib/clang/<ver>/include`, which ship the
compiler's own builtins and are best left to the consuming clang), and `off`
emits the raw aquery arguments. If the driver cannot be probed the database is
emitted unchanged.

## Tool Resolution

All tools follow the same resolution order:
Expand All @@ -107,6 +225,7 @@ devtools.configure(
lint_srcs = ["src/**/*.cpp"], # for clang_tidy (default: format_srcs)
sast_srcs = ["src/**/*.cpp"], # for sast (default: format_srcs)
ctags_srcs = ["src/**/*.cpp"], # for ctags (default: format_srcs)
vs_solution_name = "workspace", # name of the generated .sln
)
```

Expand Down
4 changes: 4 additions & 0 deletions cc/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ exports_files(
"iwyu.template.py",
"unused_deps.bzl",
"unused_deps.template.py",
"vs_solution.bzl",
"vs_solution.template.py",
"cmake.bzl",
"cmake.template.py",
"cc_devtools.bzl",
],
visibility = ["//visibility:public"],
Expand Down
19 changes: 19 additions & 0 deletions cc/cc_devtools.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ Creates these targets:
- {name}_ctags
- {name}_compile_commands
- {name}_unused_deps
- {name}_vs_solution
- {name}_cmake

Use this when you only want the C/C++ portion. For the full set
(C/C++ plus Bazel meta-tools and orchestrators) use //:devtools.bzl.
Expand All @@ -19,11 +21,13 @@ Usage:

load("//cc:clang_format.bzl", "clang_format")
load("//cc:clang_tidy.bzl", "clang_tidy")
load("//cc:cmake.bzl", "cmake")
load("//cc:compile_commands.bzl", "compile_commands")
load("//cc:ctags.bzl", "ctags")
load("//cc:iwyu.bzl", "iwyu")
load("//cc:sast.bzl", "sast")
load("//cc:unused_deps.bzl", "unused_deps")
load("//cc:vs_solution.bzl", "vs_solution")

def cc_devtools(
name,
Expand All @@ -44,6 +48,7 @@ def cc_devtools(
exclude_headers = None,
exclude_external_sources = False,
enable_cscope = False,
vs_solution_name = None,
**kwargs):
"""Create the C/C++ devtools target set.

Expand All @@ -66,6 +71,7 @@ def cc_devtools(
exclude_headers: Header exclusion mode for compile_commands.
exclude_external_sources: Exclude external sources from compile_commands.
enable_cscope: Generate cscope database alongside ctags.
vs_solution_name: Base name for the generated Visual Studio .sln. Default "workspace".
**kwargs: Additional common attributes passed to all targets.
"""
compile_commands(
Expand Down Expand Up @@ -120,3 +126,16 @@ def cc_devtools(
targets = targets if type(targets) == "string" else "//...",
**kwargs
)

vs_solution(
name = name + "_vs_solution",
targets = targets,
solution_name = vs_solution_name,
**kwargs
)

cmake(
name = name + "_cmake",
targets = targets,
**kwargs
)
Loading