asmdiff 0.1.0__tar.gz → 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- asmdiff-0.1.2/.claude/settings.local.json +9 -0
- asmdiff-0.1.2/.gitignore +2 -0
- asmdiff-0.1.2/CLAUDE.md +28 -0
- asmdiff-0.1.0/README.md → asmdiff-0.1.2/PKG-INFO +121 -16
- asmdiff-0.1.0/PKG-INFO → asmdiff-0.1.2/README.md +103 -34
- asmdiff-0.1.2/asmdiff.example.toml +50 -0
- {asmdiff-0.1.0 → asmdiff-0.1.2}/asmdiff.py +246 -19
- {asmdiff-0.1.0 → asmdiff-0.1.2}/pyproject.toml +6 -2
- {asmdiff-0.1.0 → asmdiff-0.1.2}/test_asmdiff.py +339 -0
- asmdiff-0.1.0/.claude/settings.local.json +0 -8
- asmdiff-0.1.0/asmdiff.example.toml +0 -36
- {asmdiff-0.1.0 → asmdiff-0.1.2}/LICENSE +0 -0
- {asmdiff-0.1.0 → asmdiff-0.1.2}/asmdiff_example.c +0 -0
asmdiff-0.1.2/.gitignore
ADDED
asmdiff-0.1.2/CLAUDE.md
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# asmdiff
|
|
2
|
+
|
|
3
|
+
Single-file, stdlib-only Python CLI that compares per-function assembly
|
|
4
|
+
between paired C implementations across a compiler matrix. Published to
|
|
5
|
+
PyPI as `asmdiff` (hatchling build, see pyproject.toml).
|
|
6
|
+
|
|
7
|
+
## Toolchain
|
|
8
|
+
|
|
9
|
+
- Runtime: Python >= 3.8; config files (asmdiff.toml) need >= 3.11 (tomllib).
|
|
10
|
+
- No third-party dependencies, in source or tests. Keep it that way.
|
|
11
|
+
- Compilers invoked at runtime: whatever the user's matrix names
|
|
12
|
+
(host gcc/clang, xtensa/riscv ESP toolchains via glob patterns).
|
|
13
|
+
|
|
14
|
+
## Commands
|
|
15
|
+
|
|
16
|
+
- Run tests: `python3 test_asmdiff.py` (unittest; add `-v` for detail).
|
|
17
|
+
- Run the tool: `python3 asmdiff.py asmdiff_example.c`.
|
|
18
|
+
- Everything is safe/reversible: the tool only compiles to `-S` in a
|
|
19
|
+
temp dir and prints. No flashing, no shared infra.
|
|
20
|
+
|
|
21
|
+
## Layout
|
|
22
|
+
|
|
23
|
+
- `asmdiff.py` — the entire tool.
|
|
24
|
+
- `test_asmdiff.py` — unit tests, one class per feature area.
|
|
25
|
+
- `asmdiff.example.toml` — documented starter config; keep it generic
|
|
26
|
+
(portable gcc/clang defaults, ESP targets as commented examples).
|
|
27
|
+
- README.md documents every user-visible behavior; changes to flags or
|
|
28
|
+
config keys update README, example toml, and the module docstring.
|
|
@@ -1,7 +1,30 @@
|
|
|
1
|
-
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: asmdiff
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: Compare per-function assembly between paired C implementations
|
|
5
|
+
Project-URL: Homepage, https://github.com/rt-rtos/asmdiff
|
|
6
|
+
Project-URL: Repository, https://github.com/rt-rtos/asmdiff
|
|
7
|
+
Author: Rasmus Tikkanen
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Keywords: arm,assembly,cfg,clang,cli,codegen,compiler,devtools,diff,disassembly,dsp,embedded,firmware,gcc,loop-analysis,riscv,static-analysis,xtensa
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Software Development :: Compilers
|
|
15
|
+
Classifier: Topic :: Software Development :: Debuggers
|
|
16
|
+
Requires-Python: >=3.8
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
|
|
19
|
+
# [asmdiff](https://pypi.org/project/asmdiff/)
|
|
2
20
|
## per-function assembly comparison for paired C implementations
|
|
3
21
|
|
|
4
|
-
> asmdiff is a command-line tool for comparing the generated assembly of individual C functions across implementations, compiler flags, compiler versions, and source revisions. It is intended for investigating compiler code generation rather than benchmarking runtime performance.
|
|
22
|
+
> asmdiff is a stdlib only command-line tool for comparing the generated assembly of individual C functions across implementations, compiler flags, compiler versions, and source revisions. It is intended for investigating compiler code generation rather than benchmarking runtime performance.
|
|
23
|
+
|
|
24
|
+
### Try it yourself:
|
|
25
|
+
|
|
26
|
+
`$ uvx asmdiff` / `$ pipx asmdiff`
|
|
27
|
+
---
|
|
5
28
|
|
|
6
29
|
`asmdiff.py` answers one question fast: **when I rewrite a C construct, what
|
|
7
30
|
does the compiler actually emit - before and after?** It compiles a small
|
|
@@ -10,12 +33,9 @@ assembly, and prints side-by-side listings plus a summary of instruction
|
|
|
10
33
|
counts, outbound calls, and loop spans.
|
|
11
34
|
|
|
12
35
|
Compilers and flags are configured per project through named targets in an
|
|
13
|
-
`asmdiff.toml` file
|
|
14
|
-
parses any GNU-as ELF assembly.
|
|
36
|
+
`asmdiff.toml` file and parses any GNU-as ELF assembly.
|
|
15
37
|
|
|
16
|
-
|
|
17
|
-
(e.g. `x * exp2f(5)` → one multiply) turns into a library call (e.g.
|
|
18
|
-
`ldexpf(x, 5)` → `jmp ldexpf@PLT`) after a "cleanup". That distinction is
|
|
38
|
+
Whether something constant folds or turns into a libcall is a distinction that is
|
|
19
39
|
invisible in source review and decisive on hot paths.
|
|
20
40
|
|
|
21
41
|
## Quick start
|
|
@@ -74,7 +94,7 @@ $ asmdiff.py asmdiff_example.c
|
|
|
74
94
|
```
|
|
75
95
|
asmdiff.py SOURCE.c [SOURCE2.c] [--pair OLD:NEW]... [--across FUNC]...
|
|
76
96
|
[--cc 'CC FLAGS']... [--target NAME]... [--config PATH]
|
|
77
|
-
[-- EXTRA_FLAGS...]
|
|
97
|
+
[--compile-commands [PATH]] [-- EXTRA_FLAGS...]
|
|
78
98
|
```
|
|
79
99
|
|
|
80
100
|
| Option | Meaning |
|
|
@@ -85,6 +105,7 @@ asmdiff.py SOURCE.c [SOURCE2.c] [--pair OLD:NEW]... [--across FUNC]...
|
|
|
85
105
|
| `--cc 'CC FLAGS'` | One compiler invocation, command and flags in a single quoted string. Repeatable to build a matrix. |
|
|
86
106
|
| `--target NAME` | A named target from the config file, resolved to a `--cc` entry. Repeatable; appended to the matrix after `--cc` entries. |
|
|
87
107
|
| `--config PATH` | Config file to use. Default search: `asmdiff.toml` next to `SOURCE.c`, then in the current directory, then `~/.config/`. First hit wins. |
|
|
108
|
+
| `--compile-commands [PATH]` | Borrow each source's include/define flags from a `compile_commands.json`; with no `PATH`, search `./`, `./build`, `../`, `../build`. See [below](#borrowing-includes-from-compile_commandsjson). |
|
|
88
109
|
| `-- FLAGS...` | Everything after a bare `--` is appended to *every* compiler invocation. |
|
|
89
110
|
|
|
90
111
|
With no `--cc` and no `--target`, the config file's top-level
|
|
@@ -119,19 +140,19 @@ each compiler+flags combination once:
|
|
|
119
140
|
|
|
120
141
|
```toml
|
|
121
142
|
# asmdiff.toml — next to your harnesses, in CWD, or in ~/.config/
|
|
122
|
-
default = "
|
|
143
|
+
default = "esp32s3" # target(s) used when no --cc/--target is given
|
|
123
144
|
|
|
124
|
-
[
|
|
145
|
+
[esp32s3] # production-like ESP32-S3 codegen
|
|
125
146
|
cc = "$HOME/.espressif/tools/xtensa-esp-elf/esp-*/xtensa-esp-elf/bin/xtensa-esp32s3-elf-gcc"
|
|
126
147
|
flags = [
|
|
127
|
-
"-O2", "-
|
|
148
|
+
"-O2", "-DMY_FEATURE", "-DNDEBUG",
|
|
128
149
|
"-Wno-strict-aliasing", "-mlongcalls",
|
|
129
|
-
"-I$HOME/project/components/
|
|
150
|
+
"-I$HOME/project/components/dsp/include",
|
|
130
151
|
]
|
|
131
152
|
|
|
132
|
-
[host
|
|
153
|
+
[host] # same defines on host gcc
|
|
133
154
|
cc = "gcc"
|
|
134
|
-
flags = ["-O2", "-
|
|
155
|
+
flags = ["-O2", "-DMY_FEATURE", "-I$HOME/project/components/dsp/include"]
|
|
135
156
|
```
|
|
136
157
|
|
|
137
158
|
`cc` values expand `~` and `$VARS` and may be glob patterns, so a config
|
|
@@ -148,8 +169,8 @@ shapes:
|
|
|
148
169
|
|
|
149
170
|
```bash
|
|
150
171
|
asmdiff.py h.c # config default target(s)
|
|
151
|
-
asmdiff.py h.c --target
|
|
152
|
-
asmdiff.py h.c --across f --target
|
|
172
|
+
asmdiff.py h.c --target esp32s3 --target host # two-target matrix
|
|
173
|
+
asmdiff.py h.c --across f --target esp32s3 --cc 'gcc -O2' # mix freely
|
|
153
174
|
```
|
|
154
175
|
|
|
155
176
|
A config placed next to your harness files travels with them: any invocation
|
|
@@ -159,6 +180,90 @@ included `asmdiff.example.toml` is a starting point. If a flag or include
|
|
|
159
180
|
path must vary per machine, that's what per-machine config files are for —
|
|
160
181
|
nothing lives in the tool.
|
|
161
182
|
|
|
183
|
+
### Borrowing includes from `compile_commands.json`
|
|
184
|
+
|
|
185
|
+
A real project source rarely compiles with a handful of `-I` flags. An
|
|
186
|
+
ESP-IDF component pulls in `freertos/FreeRTOS.h`, `esp_*` headers, and a
|
|
187
|
+
*generated* `sdkconfig.h`, reachable only through the dozens of `-I`/`-D`
|
|
188
|
+
flags the build system computes — none of which live in the source file.
|
|
189
|
+
That is why `asmdiff.py component.c` fails with `freertos/FreeRTOS.h: No
|
|
190
|
+
such file or directory`: not a wrong compiler (the xtensa toolchain ships
|
|
191
|
+
no FreeRTOS either), just missing include paths. Transcribing them by hand
|
|
192
|
+
is miserable.
|
|
193
|
+
|
|
194
|
+
So don't. Any build that uses CMake or Ninja can emit a
|
|
195
|
+
[`compile_commands.json`](https://clang.llvm.org/docs/JSONCompilationDatabase.html)
|
|
196
|
+
recording the exact flags for every source it builds (ESP-IDF writes one to
|
|
197
|
+
`build/compile_commands.json` on every `idf.py build`). Point a target at it:
|
|
198
|
+
|
|
199
|
+
```toml
|
|
200
|
+
[esp32s3-idf]
|
|
201
|
+
cc = "xtensa-esp32s3-elf-gcc"
|
|
202
|
+
flags = ["-O2", "-mlongcalls"]
|
|
203
|
+
compile_commands = "$HOME/myproject/build/compile_commands.json"
|
|
204
|
+
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Now `asmdiff.py $HOME/myproject/components/dsp/biquad.c --target esp32s3-idf`
|
|
208
|
+
finds that file's entry in the database and adds the include/define flags
|
|
209
|
+
it recorded — `-I`, `-isystem`, `-iquote`, `-idirafter`, `-include`,
|
|
210
|
+
`-imacros`, `-D`, `-U` — to this target's command. Everything else the
|
|
211
|
+
database records (its own compiler, `-O`/`-std`/`-W` flags, `-c`, `-o`, the
|
|
212
|
+
source) is ignored: **you** own the compiler and optimisation flags via
|
|
213
|
+
`cc`/`flags`; only the header environment is borrowed. That split is the
|
|
214
|
+
point — you can now sweep *your* `-O`/`-m` variations over a source that
|
|
215
|
+
only ever compiled one way under the build system.
|
|
216
|
+
|
|
217
|
+
Details that make it robust:
|
|
218
|
+
|
|
219
|
+
- **Paths are made absolute** against each entry's `directory`, so a
|
|
220
|
+
database full of build-relative `-I../include` flags still resolves when
|
|
221
|
+
asmdiff runs from anywhere.
|
|
222
|
+
- **Per source file.** The lookup keys on the source you pass (matched by
|
|
223
|
+
resolved absolute path), so two files in an `--across`/summary run each
|
|
224
|
+
get their own recorded flags.
|
|
225
|
+
- **Absent source is an error**, not a silent empty flag set — otherwise
|
|
226
|
+
you'd just hit the missing-header failure this feature exists to prevent.
|
|
227
|
+
The message flags a same-basename entry recorded under a different path.
|
|
228
|
+
- **`compile_commands` expands `~` and `$VARS`.** The `==` header always
|
|
229
|
+
prints the resolved compiler command; run with `-- -v` if you want to see
|
|
230
|
+
every include path the compiler actually received.
|
|
231
|
+
|
|
232
|
+
### Auto-discovering the database
|
|
233
|
+
|
|
234
|
+
When you run asmdiff from inside the project anyway, the path is
|
|
235
|
+
redundant. Two opt-ins skip it:
|
|
236
|
+
|
|
237
|
+
```toml
|
|
238
|
+
[esp32s3-idf]
|
|
239
|
+
cc = "xtensa-esp32s3-elf-gcc"
|
|
240
|
+
flags = ["-O2", "-mlongcalls"]
|
|
241
|
+
compile_commands = true # search instead of naming a path
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
asmdiff.py biquad.c --compile-commands # same, for any matrix
|
|
246
|
+
asmdiff.py biquad.c --compile-commands path/to/compile_commands.json
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Both search `./`, `./build`, `../`, `../build` — first hit wins — which
|
|
250
|
+
covers running from a project root or from one directory below it (a
|
|
251
|
+
component dir), with the database where CMake/idf.py leaves it. Finding
|
|
252
|
+
nothing is an error: discovery is opt-in, so if you asked for it, silence
|
|
253
|
+
would be a lie. It is never on by default — a target without
|
|
254
|
+
`compile_commands` and no `--compile-commands` flag borrows nothing.
|
|
255
|
+
|
|
256
|
+
The precedence is what you'd hope: a target that names its own
|
|
257
|
+
`compile_commands` path always keeps it; `--compile-commands` (with or
|
|
258
|
+
without a path) fills in every other matrix entry, including `--cc`
|
|
259
|
+
strings and the built-in gcc/clang fallback.
|
|
260
|
+
|
|
261
|
+
One behavioral difference: with a *discovered* database, a source that has
|
|
262
|
+
no entry is compiled without borrowed flags after a one-line stderr note,
|
|
263
|
+
instead of being an error. That keeps standalone harness files working
|
|
264
|
+
when a `build/` directory happens to sit nearby; an explicitly named
|
|
265
|
+
database still treats an absent source as the error it is.
|
|
266
|
+
|
|
162
267
|
## Whole-file summary
|
|
163
268
|
|
|
164
269
|
With no `--pair`, no `--across`, and no `old_*`/`new_*` functions to
|
|
@@ -1,25 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
Name: asmdiff
|
|
3
|
-
Version: 0.1.0
|
|
4
|
-
Summary: Compare per-function assembly between paired C implementations
|
|
5
|
-
Project-URL: Homepage, https://github.com/rt-rtos/asmdiff
|
|
6
|
-
Project-URL: Repository, https://github.com/rt-rtos/asmdiff
|
|
7
|
-
Author: Rasmus Tikkanen
|
|
8
|
-
License-Expression: MIT
|
|
9
|
-
License-File: LICENSE
|
|
10
|
-
Keywords: assembly,clang,codegen,compiler,disassembly,gcc
|
|
11
|
-
Classifier: Environment :: Console
|
|
12
|
-
Classifier: Intended Audience :: Developers
|
|
13
|
-
Classifier: Programming Language :: Python :: 3
|
|
14
|
-
Classifier: Topic :: Software Development :: Compilers
|
|
15
|
-
Classifier: Topic :: Software Development :: Debuggers
|
|
16
|
-
Requires-Python: >=3.8
|
|
17
|
-
Description-Content-Type: text/markdown
|
|
18
|
-
|
|
19
|
-
# asmdiff
|
|
1
|
+
# [asmdiff](https://pypi.org/project/asmdiff/)
|
|
20
2
|
## per-function assembly comparison for paired C implementations
|
|
21
3
|
|
|
22
|
-
> asmdiff is a command-line tool for comparing the generated assembly of individual C functions across implementations, compiler flags, compiler versions, and source revisions. It is intended for investigating compiler code generation rather than benchmarking runtime performance.
|
|
4
|
+
> asmdiff is a stdlib only command-line tool for comparing the generated assembly of individual C functions across implementations, compiler flags, compiler versions, and source revisions. It is intended for investigating compiler code generation rather than benchmarking runtime performance.
|
|
5
|
+
|
|
6
|
+
### Try it yourself:
|
|
7
|
+
|
|
8
|
+
`$ uvx asmdiff` / `$ pipx asmdiff`
|
|
9
|
+
---
|
|
23
10
|
|
|
24
11
|
`asmdiff.py` answers one question fast: **when I rewrite a C construct, what
|
|
25
12
|
does the compiler actually emit - before and after?** It compiles a small
|
|
@@ -28,12 +15,9 @@ assembly, and prints side-by-side listings plus a summary of instruction
|
|
|
28
15
|
counts, outbound calls, and loop spans.
|
|
29
16
|
|
|
30
17
|
Compilers and flags are configured per project through named targets in an
|
|
31
|
-
`asmdiff.toml` file
|
|
32
|
-
parses any GNU-as ELF assembly.
|
|
18
|
+
`asmdiff.toml` file and parses any GNU-as ELF assembly.
|
|
33
19
|
|
|
34
|
-
|
|
35
|
-
(e.g. `x * exp2f(5)` → one multiply) turns into a library call (e.g.
|
|
36
|
-
`ldexpf(x, 5)` → `jmp ldexpf@PLT`) after a "cleanup". That distinction is
|
|
20
|
+
Whether something constant folds or turns into a libcall is a distinction that is
|
|
37
21
|
invisible in source review and decisive on hot paths.
|
|
38
22
|
|
|
39
23
|
## Quick start
|
|
@@ -92,7 +76,7 @@ $ asmdiff.py asmdiff_example.c
|
|
|
92
76
|
```
|
|
93
77
|
asmdiff.py SOURCE.c [SOURCE2.c] [--pair OLD:NEW]... [--across FUNC]...
|
|
94
78
|
[--cc 'CC FLAGS']... [--target NAME]... [--config PATH]
|
|
95
|
-
[-- EXTRA_FLAGS...]
|
|
79
|
+
[--compile-commands [PATH]] [-- EXTRA_FLAGS...]
|
|
96
80
|
```
|
|
97
81
|
|
|
98
82
|
| Option | Meaning |
|
|
@@ -103,6 +87,7 @@ asmdiff.py SOURCE.c [SOURCE2.c] [--pair OLD:NEW]... [--across FUNC]...
|
|
|
103
87
|
| `--cc 'CC FLAGS'` | One compiler invocation, command and flags in a single quoted string. Repeatable to build a matrix. |
|
|
104
88
|
| `--target NAME` | A named target from the config file, resolved to a `--cc` entry. Repeatable; appended to the matrix after `--cc` entries. |
|
|
105
89
|
| `--config PATH` | Config file to use. Default search: `asmdiff.toml` next to `SOURCE.c`, then in the current directory, then `~/.config/`. First hit wins. |
|
|
90
|
+
| `--compile-commands [PATH]` | Borrow each source's include/define flags from a `compile_commands.json`; with no `PATH`, search `./`, `./build`, `../`, `../build`. See [below](#borrowing-includes-from-compile_commandsjson). |
|
|
106
91
|
| `-- FLAGS...` | Everything after a bare `--` is appended to *every* compiler invocation. |
|
|
107
92
|
|
|
108
93
|
With no `--cc` and no `--target`, the config file's top-level
|
|
@@ -137,19 +122,19 @@ each compiler+flags combination once:
|
|
|
137
122
|
|
|
138
123
|
```toml
|
|
139
124
|
# asmdiff.toml — next to your harnesses, in CWD, or in ~/.config/
|
|
140
|
-
default = "
|
|
125
|
+
default = "esp32s3" # target(s) used when no --cc/--target is given
|
|
141
126
|
|
|
142
|
-
[
|
|
127
|
+
[esp32s3] # production-like ESP32-S3 codegen
|
|
143
128
|
cc = "$HOME/.espressif/tools/xtensa-esp-elf/esp-*/xtensa-esp-elf/bin/xtensa-esp32s3-elf-gcc"
|
|
144
129
|
flags = [
|
|
145
|
-
"-O2", "-
|
|
130
|
+
"-O2", "-DMY_FEATURE", "-DNDEBUG",
|
|
146
131
|
"-Wno-strict-aliasing", "-mlongcalls",
|
|
147
|
-
"-I$HOME/project/components/
|
|
132
|
+
"-I$HOME/project/components/dsp/include",
|
|
148
133
|
]
|
|
149
134
|
|
|
150
|
-
[host
|
|
135
|
+
[host] # same defines on host gcc
|
|
151
136
|
cc = "gcc"
|
|
152
|
-
flags = ["-O2", "-
|
|
137
|
+
flags = ["-O2", "-DMY_FEATURE", "-I$HOME/project/components/dsp/include"]
|
|
153
138
|
```
|
|
154
139
|
|
|
155
140
|
`cc` values expand `~` and `$VARS` and may be glob patterns, so a config
|
|
@@ -166,8 +151,8 @@ shapes:
|
|
|
166
151
|
|
|
167
152
|
```bash
|
|
168
153
|
asmdiff.py h.c # config default target(s)
|
|
169
|
-
asmdiff.py h.c --target
|
|
170
|
-
asmdiff.py h.c --across f --target
|
|
154
|
+
asmdiff.py h.c --target esp32s3 --target host # two-target matrix
|
|
155
|
+
asmdiff.py h.c --across f --target esp32s3 --cc 'gcc -O2' # mix freely
|
|
171
156
|
```
|
|
172
157
|
|
|
173
158
|
A config placed next to your harness files travels with them: any invocation
|
|
@@ -177,6 +162,90 @@ included `asmdiff.example.toml` is a starting point. If a flag or include
|
|
|
177
162
|
path must vary per machine, that's what per-machine config files are for —
|
|
178
163
|
nothing lives in the tool.
|
|
179
164
|
|
|
165
|
+
### Borrowing includes from `compile_commands.json`
|
|
166
|
+
|
|
167
|
+
A real project source rarely compiles with a handful of `-I` flags. An
|
|
168
|
+
ESP-IDF component pulls in `freertos/FreeRTOS.h`, `esp_*` headers, and a
|
|
169
|
+
*generated* `sdkconfig.h`, reachable only through the dozens of `-I`/`-D`
|
|
170
|
+
flags the build system computes — none of which live in the source file.
|
|
171
|
+
That is why `asmdiff.py component.c` fails with `freertos/FreeRTOS.h: No
|
|
172
|
+
such file or directory`: not a wrong compiler (the xtensa toolchain ships
|
|
173
|
+
no FreeRTOS either), just missing include paths. Transcribing them by hand
|
|
174
|
+
is miserable.
|
|
175
|
+
|
|
176
|
+
So don't. Any build that uses CMake or Ninja can emit a
|
|
177
|
+
[`compile_commands.json`](https://clang.llvm.org/docs/JSONCompilationDatabase.html)
|
|
178
|
+
recording the exact flags for every source it builds (ESP-IDF writes one to
|
|
179
|
+
`build/compile_commands.json` on every `idf.py build`). Point a target at it:
|
|
180
|
+
|
|
181
|
+
```toml
|
|
182
|
+
[esp32s3-idf]
|
|
183
|
+
cc = "xtensa-esp32s3-elf-gcc"
|
|
184
|
+
flags = ["-O2", "-mlongcalls"]
|
|
185
|
+
compile_commands = "$HOME/myproject/build/compile_commands.json"
|
|
186
|
+
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Now `asmdiff.py $HOME/myproject/components/dsp/biquad.c --target esp32s3-idf`
|
|
190
|
+
finds that file's entry in the database and adds the include/define flags
|
|
191
|
+
it recorded — `-I`, `-isystem`, `-iquote`, `-idirafter`, `-include`,
|
|
192
|
+
`-imacros`, `-D`, `-U` — to this target's command. Everything else the
|
|
193
|
+
database records (its own compiler, `-O`/`-std`/`-W` flags, `-c`, `-o`, the
|
|
194
|
+
source) is ignored: **you** own the compiler and optimisation flags via
|
|
195
|
+
`cc`/`flags`; only the header environment is borrowed. That split is the
|
|
196
|
+
point — you can now sweep *your* `-O`/`-m` variations over a source that
|
|
197
|
+
only ever compiled one way under the build system.
|
|
198
|
+
|
|
199
|
+
Details that make it robust:
|
|
200
|
+
|
|
201
|
+
- **Paths are made absolute** against each entry's `directory`, so a
|
|
202
|
+
database full of build-relative `-I../include` flags still resolves when
|
|
203
|
+
asmdiff runs from anywhere.
|
|
204
|
+
- **Per source file.** The lookup keys on the source you pass (matched by
|
|
205
|
+
resolved absolute path), so two files in an `--across`/summary run each
|
|
206
|
+
get their own recorded flags.
|
|
207
|
+
- **Absent source is an error**, not a silent empty flag set — otherwise
|
|
208
|
+
you'd just hit the missing-header failure this feature exists to prevent.
|
|
209
|
+
The message flags a same-basename entry recorded under a different path.
|
|
210
|
+
- **`compile_commands` expands `~` and `$VARS`.** The `==` header always
|
|
211
|
+
prints the resolved compiler command; run with `-- -v` if you want to see
|
|
212
|
+
every include path the compiler actually received.
|
|
213
|
+
|
|
214
|
+
### Auto-discovering the database
|
|
215
|
+
|
|
216
|
+
When you run asmdiff from inside the project anyway, the path is
|
|
217
|
+
redundant. Two opt-ins skip it:
|
|
218
|
+
|
|
219
|
+
```toml
|
|
220
|
+
[esp32s3-idf]
|
|
221
|
+
cc = "xtensa-esp32s3-elf-gcc"
|
|
222
|
+
flags = ["-O2", "-mlongcalls"]
|
|
223
|
+
compile_commands = true # search instead of naming a path
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
```bash
|
|
227
|
+
asmdiff.py biquad.c --compile-commands # same, for any matrix
|
|
228
|
+
asmdiff.py biquad.c --compile-commands path/to/compile_commands.json
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
Both search `./`, `./build`, `../`, `../build` — first hit wins — which
|
|
232
|
+
covers running from a project root or from one directory below it (a
|
|
233
|
+
component dir), with the database where CMake/idf.py leaves it. Finding
|
|
234
|
+
nothing is an error: discovery is opt-in, so if you asked for it, silence
|
|
235
|
+
would be a lie. It is never on by default — a target without
|
|
236
|
+
`compile_commands` and no `--compile-commands` flag borrows nothing.
|
|
237
|
+
|
|
238
|
+
The precedence is what you'd hope: a target that names its own
|
|
239
|
+
`compile_commands` path always keeps it; `--compile-commands` (with or
|
|
240
|
+
without a path) fills in every other matrix entry, including `--cc`
|
|
241
|
+
strings and the built-in gcc/clang fallback.
|
|
242
|
+
|
|
243
|
+
One behavioral difference: with a *discovered* database, a source that has
|
|
244
|
+
no entry is compiled without borrowed flags after a one-line stderr note,
|
|
245
|
+
instead of being an error. That keeps standalone harness files working
|
|
246
|
+
when a `build/` directory happens to sit nearby; an explicitly named
|
|
247
|
+
database still treats an absent source as the error it is.
|
|
248
|
+
|
|
180
249
|
## Whole-file summary
|
|
181
250
|
|
|
182
251
|
With no `--pair`, no `--across`, and no `old_*`/`new_*` functions to
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# asmdiff config — named compiler+flags targets.
|
|
2
|
+
#
|
|
3
|
+
# Copy to one of the locations asmdiff searches (first hit wins):
|
|
4
|
+
# 1. the path given with --config
|
|
5
|
+
# 2. asmdiff.toml next to the SOURCE.c being compiled
|
|
6
|
+
# 3. asmdiff.toml in the current directory
|
|
7
|
+
# 4. ~/.config/asmdiff.toml
|
|
8
|
+
#
|
|
9
|
+
# Each [table] is a target usable as `--target NAME`; the optional
|
|
10
|
+
# top-level `default` names the target(s) used when no --cc/--target
|
|
11
|
+
# is given (a list runs several: default = ["gcc", "clang"]).
|
|
12
|
+
# Compile at the flags your project ships with — that is the whole
|
|
13
|
+
# point of the tool.
|
|
14
|
+
|
|
15
|
+
default = "gcc"
|
|
16
|
+
|
|
17
|
+
[gcc]
|
|
18
|
+
cc = "gcc"
|
|
19
|
+
flags = [
|
|
20
|
+
"-O3", "-Wall", "-Wextra",
|
|
21
|
+
# Project-specific include paths and defines go here:
|
|
22
|
+
# "-I/path/to/project/src", "-DMY_FEATURE",
|
|
23
|
+
]
|
|
24
|
+
|
|
25
|
+
[clang]
|
|
26
|
+
cc = "clang"
|
|
27
|
+
flags = ["-O3", "-Wall", "-Wextra"]
|
|
28
|
+
|
|
29
|
+
# cc values may use ~, $VARS, and glob patterns. A glob that matches
|
|
30
|
+
# several installed toolchains resolves to the highest version-sorted
|
|
31
|
+
# one (announced on stderr); pin the exact esp-NN directory instead if
|
|
32
|
+
# reproducibility across sessions matters more than convenience.
|
|
33
|
+
[esp32s3]
|
|
34
|
+
cc = "$HOME/.espressif/tools/xtensa-esp-elf/esp-*/xtensa-esp-elf/bin/xtensa-esp32s3-elf-gcc"
|
|
35
|
+
flags = ["-O2", "-mlongcalls", "-DNDEBUG"]
|
|
36
|
+
|
|
37
|
+
# A project source that pulls in framework headers (ESP-IDF's freertos/*,
|
|
38
|
+
# esp_*, a generated sdkconfig.h, ...) needs the dozens of -I/-D flags its
|
|
39
|
+
# build system passes. Point `compile_commands` at the database that build
|
|
40
|
+
# emits (idf.py leaves build/compile_commands.json), and asmdiff copies the
|
|
41
|
+
# include/define flags recorded there for the file being compiled — so you
|
|
42
|
+
# never transcribe include paths by hand. The compiler and -O/-m flags
|
|
43
|
+
# still come from cc/flags; only the header environment is borrowed.
|
|
44
|
+
# A path expands ~ and $VARS; `compile_commands = true` instead searches
|
|
45
|
+
# ./, ./build, ../, ../build at run time (a source missing from a database
|
|
46
|
+
# found that way is compiled without borrowed flags, with a note).
|
|
47
|
+
[esp32s3-idf]
|
|
48
|
+
cc = "$HOME/.espressif/tools/xtensa-esp-elf/esp-*/xtensa-esp-elf/bin/xtensa-esp32s3-elf-gcc"
|
|
49
|
+
flags = ["-O2", "-mlongcalls"]
|
|
50
|
+
compile_commands = true
|
|
@@ -12,6 +12,7 @@ Usage:
|
|
|
12
12
|
tools/asmdiff/asmdiff.py SOURCE.c [SOURCE2.c] [--pair OLD:NEW]...
|
|
13
13
|
[--across FUNC]... [--cc 'CC FLAGS']...
|
|
14
14
|
[--target NAME]... [--config PATH]
|
|
15
|
+
[--compile-commands [PATH]]
|
|
15
16
|
[-- EXTRA_FLAGS...]
|
|
16
17
|
|
|
17
18
|
Three modes:
|
|
@@ -28,10 +29,20 @@ Compilers come from --cc strings, from named targets in an asmdiff.toml
|
|
|
28
29
|
config file (--target NAME), from the config's `default` entry, or —
|
|
29
30
|
failing all of those — plain `gcc -O3` and `clang -O3`.
|
|
30
31
|
Flags after a bare `--` are appended to every compiler invocation.
|
|
32
|
+
|
|
33
|
+
A config target may name a compile_commands.json (compile_commands = PATH):
|
|
34
|
+
the include/define flags recorded there for the source being compiled are
|
|
35
|
+
added to that target's command, so a real project source resolves its
|
|
36
|
+
headers the way its own build system does (e.g. an ESP-IDF component).
|
|
37
|
+
compile_commands = true in a target — or a bare --compile-commands on the
|
|
38
|
+
command line — instead searches ./, ./build, ../, ../build for the
|
|
39
|
+
database; a source absent from a database found that way is compiled
|
|
40
|
+
without borrowed flags (with a note) rather than being an error.
|
|
31
41
|
"""
|
|
32
42
|
import argparse
|
|
33
43
|
import glob
|
|
34
44
|
import hashlib
|
|
45
|
+
import json
|
|
35
46
|
import os
|
|
36
47
|
import re
|
|
37
48
|
import shlex
|
|
@@ -51,6 +62,171 @@ DEFAULT_COMPILERS = ["gcc", "clang"]
|
|
|
51
62
|
FALLBACK_FLAGS = "-O3"
|
|
52
63
|
CONFIG_NAME = "asmdiff.toml"
|
|
53
64
|
|
|
65
|
+
# Preprocessor flags lifted from a compile_commands.json entry so a project
|
|
66
|
+
# source compiles the way its build system compiles it: header search paths,
|
|
67
|
+
# forced includes, and defines. Everything else the entry records (the
|
|
68
|
+
# compiler, -O/-std/-W flags, -c, -o OUT, the source itself) is ignored —
|
|
69
|
+
# asmdiff supplies the compiler and optimisation flags from the target.
|
|
70
|
+
# Path-bearing flags may be glued (-Ipath) or split (-I path); defines too
|
|
71
|
+
# (-DFOO / -D FOO). None of the path flags is a prefix of another and -I is
|
|
72
|
+
# case-distinct from -i*, so a single left-to-right scan is unambiguous.
|
|
73
|
+
_CC_DB_PATH_FLAGS = ("-I", "-iquote", "-isystem", "-idirafter",
|
|
74
|
+
"-include", "-imacros", "-isysroot")
|
|
75
|
+
_CC_DB_PLAIN_FLAGS = ("-D", "-U")
|
|
76
|
+
_DB_CACHE = {}
|
|
77
|
+
_MISS_NOTED = set()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class Target(str):
|
|
81
|
+
"""A compiler-matrix entry: the ``CC FLAGS`` command string, plus an
|
|
82
|
+
optional compile_commands.json whose per-source include/define flags are
|
|
83
|
+
appended at compile time. Subclassing str means it prints, compares, and
|
|
84
|
+
shlex-splits as the bare command everywhere the matrix is consumed, so
|
|
85
|
+
only compile_to_asm needs to know about the extra attributes.
|
|
86
|
+
|
|
87
|
+
``db_discovered`` marks a database that was found by searching near the
|
|
88
|
+
CWD rather than named explicitly; a source absent from a discovered
|
|
89
|
+
database is tolerated (compiled without borrowed flags, with a note),
|
|
90
|
+
where an explicit database makes that an error."""
|
|
91
|
+
|
|
92
|
+
def __new__(cls, cmd, compile_commands=None, db_discovered=False):
|
|
93
|
+
self = super().__new__(cls, cmd)
|
|
94
|
+
self.compile_commands = compile_commands
|
|
95
|
+
self.db_discovered = db_discovered
|
|
96
|
+
return self
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _abs_against(directory, value):
|
|
100
|
+
"""Resolve a path recorded in a compile_commands entry against that
|
|
101
|
+
entry's ``directory``, so a relative -I still works from asmdiff's CWD."""
|
|
102
|
+
p = Path(value)
|
|
103
|
+
if directory and not p.is_absolute():
|
|
104
|
+
p = Path(directory) / p
|
|
105
|
+
return str(p)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def include_flags(tokens, directory):
|
|
109
|
+
"""Pick header-search, forced-include, and define flags out of one
|
|
110
|
+
recorded compile command; make relative paths absolute against
|
|
111
|
+
``directory``. Glued and split spellings are both recognised; path
|
|
112
|
+
flags are re-emitted in split form (-I path), which every driver accepts.
|
|
113
|
+
"""
|
|
114
|
+
flags = []
|
|
115
|
+
i, n = 0, len(tokens)
|
|
116
|
+
while i < n:
|
|
117
|
+
tok, extra, matched = tokens[i], 0, False
|
|
118
|
+
for f in _CC_DB_PATH_FLAGS:
|
|
119
|
+
if tok == f and i + 1 < n: # -I path
|
|
120
|
+
flags += [f, _abs_against(directory, tokens[i + 1])]
|
|
121
|
+
extra, matched = 1, True
|
|
122
|
+
break
|
|
123
|
+
if tok.startswith(f) and len(tok) > len(f): # -Ipath
|
|
124
|
+
flags += [f, _abs_against(directory, tok[len(f):])]
|
|
125
|
+
matched = True
|
|
126
|
+
break
|
|
127
|
+
if not matched:
|
|
128
|
+
for f in _CC_DB_PLAIN_FLAGS:
|
|
129
|
+
if tok == f and i + 1 < n: # -D FOO
|
|
130
|
+
flags.append(f + tokens[i + 1])
|
|
131
|
+
extra = 1
|
|
132
|
+
break
|
|
133
|
+
if tok.startswith(f) and len(tok) > len(f): # -DFOO
|
|
134
|
+
flags.append(tok)
|
|
135
|
+
break
|
|
136
|
+
i += 1 + extra
|
|
137
|
+
return flags
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def find_compile_commands():
|
|
141
|
+
"""Locate a compile_commands.json near the current directory.
|
|
142
|
+
|
|
143
|
+
Searched in order: ./, ./build, ../, ../build — first hit wins. This
|
|
144
|
+
covers running asmdiff from a project root or from one directory below
|
|
145
|
+
it (a component dir), with the database where CMake/idf.py leaves it.
|
|
146
|
+
Returns None when nothing is found.
|
|
147
|
+
"""
|
|
148
|
+
cwd = Path.cwd()
|
|
149
|
+
for d in (cwd, cwd / "build", cwd.parent, cwd.parent / "build"):
|
|
150
|
+
candidate = d / "compile_commands.json"
|
|
151
|
+
if candidate.is_file():
|
|
152
|
+
return str(candidate)
|
|
153
|
+
return None
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def _discovered_db(who):
|
|
157
|
+
"""find_compile_commands() for a caller that opted in (compile_commands
|
|
158
|
+
= true, or a bare --compile-commands): finding nothing is then an error,
|
|
159
|
+
not a silent no-op."""
|
|
160
|
+
found = find_compile_commands()
|
|
161
|
+
if found is None:
|
|
162
|
+
sys.exit(f"error: {who}: no compile_commands.json found in "
|
|
163
|
+
"./, ./build, ../, or ../build")
|
|
164
|
+
return found
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
def load_compile_commands(path):
|
|
168
|
+
"""Parse a compile_commands.json into its list of entries (cached, since
|
|
169
|
+
one database is queried once per source per compiler in the matrix)."""
|
|
170
|
+
if path in _DB_CACHE:
|
|
171
|
+
return _DB_CACHE[path]
|
|
172
|
+
try:
|
|
173
|
+
data = json.loads(Path(path).read_text())
|
|
174
|
+
except FileNotFoundError:
|
|
175
|
+
sys.exit(f"error: compile_commands.json not found: {path}")
|
|
176
|
+
except (json.JSONDecodeError, OSError) as exc:
|
|
177
|
+
sys.exit(f"error: {path}: {exc}")
|
|
178
|
+
if not isinstance(data, list):
|
|
179
|
+
sys.exit(f"error: {path}: expected a JSON array of compile entries")
|
|
180
|
+
_DB_CACHE[path] = data
|
|
181
|
+
return data
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def compile_commands_flags(db_path, source, missing_ok=False):
|
|
185
|
+
"""Header/define flags for ``source`` taken from a compile_commands.json.
|
|
186
|
+
|
|
187
|
+
The entry whose ``file`` resolves to the same path as ``source`` supplies
|
|
188
|
+
its include/define flags (relative paths made absolute against the entry's
|
|
189
|
+
``directory``). A ``command`` string is tokenised; an ``arguments`` array
|
|
190
|
+
is used as-is. A source absent from the database is an error — compiling
|
|
191
|
+
it would otherwise fail on the very header this feature exists to supply —
|
|
192
|
+
and the message flags a same-name entry recorded under a different path.
|
|
193
|
+
With ``missing_ok`` (auto-discovered databases) an absent source instead
|
|
194
|
+
compiles without borrowed flags, after a one-time note per (db, source).
|
|
195
|
+
"""
|
|
196
|
+
entries = load_compile_commands(db_path)
|
|
197
|
+
want = Path(source).resolve()
|
|
198
|
+
name_seen = False
|
|
199
|
+
for entry in entries:
|
|
200
|
+
f = entry.get("file")
|
|
201
|
+
if not f:
|
|
202
|
+
continue
|
|
203
|
+
directory = entry.get("directory", "")
|
|
204
|
+
fp = Path(f)
|
|
205
|
+
if not fp.is_absolute():
|
|
206
|
+
fp = Path(directory) / fp
|
|
207
|
+
try:
|
|
208
|
+
same = fp.resolve() == want
|
|
209
|
+
except OSError:
|
|
210
|
+
same = False
|
|
211
|
+
if same:
|
|
212
|
+
args = entry.get("arguments")
|
|
213
|
+
tokens = (list(args) if isinstance(args, list)
|
|
214
|
+
else shlex.split(entry.get("command", "")))
|
|
215
|
+
return include_flags(tokens, directory)
|
|
216
|
+
if Path(f).name == want.name:
|
|
217
|
+
name_seen = True
|
|
218
|
+
if missing_ok:
|
|
219
|
+
key = (db_path, str(want))
|
|
220
|
+
if key not in _MISS_NOTED:
|
|
221
|
+
_MISS_NOTED.add(key)
|
|
222
|
+
print(f"note: {source} not in {db_path}; "
|
|
223
|
+
"compiling without its include/define flags",
|
|
224
|
+
file=sys.stderr)
|
|
225
|
+
return []
|
|
226
|
+
hint = (f"; an entry named {want.name} exists under a different path — "
|
|
227
|
+
"pass the source as it appears in the database") if name_seen else ""
|
|
228
|
+
sys.exit(f"error: {source} not found in {db_path}{hint}")
|
|
229
|
+
|
|
54
230
|
# A label at column 0 that is not a local (.L*) label starts a function.
|
|
55
231
|
FUNC_LABEL = re.compile(r"^([A-Za-z_][\w$.]*):")
|
|
56
232
|
# Assembler directives that carry no information worth reading.
|
|
@@ -60,6 +236,15 @@ NOISE = re.compile(
|
|
|
60
236
|
)
|
|
61
237
|
# Compiler-generated bracketing labels that add nothing (.LFB0:, .Lfunc_end0:).
|
|
62
238
|
NOISE_LABEL = re.compile(r"^\.(LFB|LFE|Lfunc_begin|Lfunc_end)\d*:")
|
|
239
|
+
# Data emitted *inside* a function body: switch jump tables (.long/.word
|
|
240
|
+
# entries), inline constants, strings. These are not instructions, so they
|
|
241
|
+
# must not be counted; and a self-relative table entry (".long .L5-.L4")
|
|
242
|
+
# references its base label from below, which the loop-span scan would
|
|
243
|
+
# otherwise read as a backward branch and report as a phantom loop.
|
|
244
|
+
DATA = re.compile(
|
|
245
|
+
r"^\.(long|quad|word|hword|short|byte|[248]byte|value|zero|octa|"
|
|
246
|
+
r"string|ascii|asciz|single|double|float|dc(\.[abwlq])?)\b"
|
|
247
|
+
)
|
|
63
248
|
|
|
64
249
|
|
|
65
250
|
def extract_functions(asm_text):
|
|
@@ -67,9 +252,10 @@ def extract_functions(asm_text):
|
|
|
67
252
|
|
|
68
253
|
A function body runs from its column-0 label to the matching .size
|
|
69
254
|
directive (gcc and clang both emit one on ELF) or the next function
|
|
70
|
-
label. Comment lines, CFI/section/alignment directives,
|
|
71
|
-
|
|
72
|
-
local labels (loop targets) are
|
|
255
|
+
label. Comment lines, CFI/section/alignment directives, compiler
|
|
256
|
+
bracketing labels, and inline data (switch jump tables, constants) are
|
|
257
|
+
dropped; instructions and meaningful local labels (loop targets) are
|
|
258
|
+
kept, whitespace-stripped.
|
|
73
259
|
"""
|
|
74
260
|
funcs = {}
|
|
75
261
|
current = None
|
|
@@ -87,7 +273,7 @@ def extract_functions(asm_text):
|
|
|
87
273
|
line = raw.strip()
|
|
88
274
|
if not line or line.startswith(("#", "//")):
|
|
89
275
|
continue
|
|
90
|
-
if NOISE.match(line) or NOISE_LABEL.match(line):
|
|
276
|
+
if NOISE.match(line) or NOISE_LABEL.match(line) or DATA.match(line):
|
|
91
277
|
continue
|
|
92
278
|
funcs[current].append(line)
|
|
93
279
|
return funcs
|
|
@@ -257,26 +443,52 @@ def target_command(config, name, config_path):
|
|
|
257
443
|
if isinstance(flags, str) or not all(isinstance(f, str) for f in flags):
|
|
258
444
|
sys.exit(f"error: target [{name}]: flags must be an array of strings")
|
|
259
445
|
flags = [os.path.expandvars(f) for f in flags]
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
446
|
+
db = entry.get("compile_commands")
|
|
447
|
+
discovered = False
|
|
448
|
+
if db is True: # opt in to CWD-based discovery
|
|
449
|
+
db, discovered = _discovered_db(f"target [{name}]"), True
|
|
450
|
+
elif db is False:
|
|
451
|
+
db = None
|
|
452
|
+
elif db is not None:
|
|
453
|
+
if not isinstance(db, str):
|
|
454
|
+
sys.exit(f"error: target [{name}]: compile_commands must be a "
|
|
455
|
+
"path to a compile_commands.json, or true to search "
|
|
456
|
+
"./, ./build, ../, ../build")
|
|
457
|
+
db = os.path.expandvars(os.path.expanduser(db))
|
|
458
|
+
return Target(shlex.join([resolve_cc(cc, name), *flags]), db, discovered)
|
|
459
|
+
|
|
460
|
+
|
|
461
|
+
def build_matrix(cc_args, target_args, config, config_path, db_arg=None):
|
|
264
462
|
"""Resolve the compiler matrix.
|
|
265
463
|
|
|
266
464
|
--cc strings verbatim, then --target entries, in that order. With
|
|
267
465
|
neither, the config's `default` (a target name or list of names);
|
|
268
466
|
with no config or no default, plain gcc/clang at -O3.
|
|
467
|
+
|
|
468
|
+
``db_arg`` is the --compile-commands value: a PATH applies that
|
|
469
|
+
database to every entry, True discovers one near the CWD. A target
|
|
470
|
+
whose config names its own compile_commands keeps it.
|
|
269
471
|
"""
|
|
270
472
|
entries = list(cc_args)
|
|
271
473
|
entries += [target_command(config, name, config_path)
|
|
272
474
|
for name in target_args]
|
|
273
|
-
if entries:
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
475
|
+
if not entries:
|
|
476
|
+
default = (config or {}).get("default")
|
|
477
|
+
if default:
|
|
478
|
+
names = [default] if isinstance(default, str) else list(default)
|
|
479
|
+
entries = [target_command(config, name, config_path)
|
|
480
|
+
for name in names]
|
|
481
|
+
else:
|
|
482
|
+
entries = [f"{cc} {FALLBACK_FLAGS}" for cc in DEFAULT_COMPILERS]
|
|
483
|
+
if db_arg is not None:
|
|
484
|
+
if db_arg is True:
|
|
485
|
+
db, discovered = _discovered_db("--compile-commands"), True
|
|
486
|
+
else:
|
|
487
|
+
db = os.path.expandvars(os.path.expanduser(db_arg))
|
|
488
|
+
discovered = False
|
|
489
|
+
entries = [e if getattr(e, "compile_commands", None) is not None
|
|
490
|
+
else Target(e, db, discovered) for e in entries]
|
|
491
|
+
return entries
|
|
280
492
|
|
|
281
493
|
|
|
282
494
|
def asm_output_name(cc_cmd, harness):
|
|
@@ -295,7 +507,9 @@ def asm_output_name(cc_cmd, harness):
|
|
|
295
507
|
def compile_to_asm(cc_cmd, extra_flags, harness, out_dir):
|
|
296
508
|
"""Run one compiler to -S; return the asm text.
|
|
297
509
|
|
|
298
|
-
|
|
510
|
+
If the matrix entry names a compile_commands.json, this source's
|
|
511
|
+
include/define flags from that database are inserted before any bare-``--``
|
|
512
|
+
flags. Returns None (with a warning) if the compiler is not on PATH.
|
|
299
513
|
Exits with the compiler's stderr on a compile failure.
|
|
300
514
|
"""
|
|
301
515
|
argv = shlex.split(cc_cmd)
|
|
@@ -303,8 +517,14 @@ def compile_to_asm(cc_cmd, extra_flags, harness, out_dir):
|
|
|
303
517
|
print(f"warning: {argv[0]} not found on PATH, skipping",
|
|
304
518
|
file=sys.stderr)
|
|
305
519
|
return None
|
|
306
|
-
|
|
307
|
-
|
|
520
|
+
db = getattr(cc_cmd, "compile_commands", None)
|
|
521
|
+
db_flags = (compile_commands_flags(
|
|
522
|
+
db, harness,
|
|
523
|
+
missing_ok=getattr(cc_cmd, "db_discovered", False))
|
|
524
|
+
if db else [])
|
|
525
|
+
out_s = Path(out_dir) / asm_output_name(shlex.join(argv + db_flags), harness)
|
|
526
|
+
cmd = (argv + db_flags + list(extra_flags)
|
|
527
|
+
+ ["-S", "-o", str(out_s), str(harness)])
|
|
308
528
|
proc = subprocess.run(cmd, capture_output=True, text=True)
|
|
309
529
|
if proc.returncode != 0:
|
|
310
530
|
sys.exit(f"error: compile failed: {' '.join(cmd)}\n{proc.stderr}")
|
|
@@ -539,6 +759,12 @@ def main(argv=None):
|
|
|
539
759
|
help="named [table] from the config file, resolved "
|
|
540
760
|
"to a --cc entry (repeatable; appended to the "
|
|
541
761
|
"matrix after --cc entries)")
|
|
762
|
+
parser.add_argument("--compile-commands", nargs="?", const=True,
|
|
763
|
+
default=None, metavar="PATH",
|
|
764
|
+
help="borrow each source's include/define flags from "
|
|
765
|
+
"a compile_commands.json; with no PATH, search "
|
|
766
|
+
"./, ./build, ../, ../build. A target whose "
|
|
767
|
+
"config names its own compile_commands keeps it")
|
|
542
768
|
parser.add_argument("--config", metavar="PATH",
|
|
543
769
|
help=f"config file; default search: {CONFIG_NAME} "
|
|
544
770
|
"next to SOURCE.c, in the current directory, "
|
|
@@ -554,7 +780,8 @@ def main(argv=None):
|
|
|
554
780
|
"use --across FUNC for two files")
|
|
555
781
|
config_path = find_config(args.config, args.sources)
|
|
556
782
|
config = load_config(config_path) if config_path else None
|
|
557
|
-
matrix = build_matrix(args.cc, args.target, config, config_path
|
|
783
|
+
matrix = build_matrix(args.cc, args.target, config, config_path,
|
|
784
|
+
args.compile_commands)
|
|
558
785
|
if args.across and len(args.sources) == 1 and len(matrix) < 2:
|
|
559
786
|
parser.error("--across on one file needs at least two --cc entries")
|
|
560
787
|
for spec in args.pair:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "asmdiff"
|
|
7
|
-
version = "0.1.
|
|
7
|
+
version = "0.1.2"
|
|
8
8
|
description = "Compare per-function assembly between paired C implementations"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -13,7 +13,11 @@ requires-python = ">=3.8"
|
|
|
13
13
|
authors = [
|
|
14
14
|
{ name = "Rasmus Tikkanen" },
|
|
15
15
|
]
|
|
16
|
-
keywords = [
|
|
16
|
+
keywords = [
|
|
17
|
+
"assembly", "compiler", "gcc", "clang", "disassembly", "codegen",
|
|
18
|
+
"diff", "static-analysis", "embedded", "firmware", "dsp",
|
|
19
|
+
"xtensa", "arm", "riscv", "cli", "devtools", "loop-analysis", "cfg"
|
|
20
|
+
]
|
|
17
21
|
classifiers = [
|
|
18
22
|
"Environment :: Console",
|
|
19
23
|
"Intended Audience :: Developers",
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"""Unit tests for asmdiff.py. Run: python3 tools/asmdiff/test_asmdiff.py -v"""
|
|
3
3
|
import contextlib
|
|
4
4
|
import io
|
|
5
|
+
import json
|
|
5
6
|
import os
|
|
6
7
|
import tempfile
|
|
7
8
|
import unittest
|
|
@@ -74,6 +75,77 @@ looper:
|
|
|
74
75
|
\t.size\tlooper, .-looper
|
|
75
76
|
"""
|
|
76
77
|
|
|
78
|
+
# A switch lowered to a jump table, faithful to `gcc -O2 -S`. The table is
|
|
79
|
+
# emitted *inside* the function body (between the label and `.size`) via a
|
|
80
|
+
# .rodata/.text toggle, with self-relative entries `.long .Lx-.L4` — data,
|
|
81
|
+
# not instructions, and their .L4 operand must not read as a backward branch.
|
|
82
|
+
SWITCH_ASM = """\
|
|
83
|
+
\t.globl\tsel
|
|
84
|
+
\t.type\tsel, @function
|
|
85
|
+
sel:
|
|
86
|
+
.LFB0:
|
|
87
|
+
\t.cfi_startproc
|
|
88
|
+
\tendbr64
|
|
89
|
+
\tcmpl\t$4, %edi
|
|
90
|
+
\tja\t.L9
|
|
91
|
+
\tleaq\t.L4(%rip), %rcx
|
|
92
|
+
\tmovl\t%edi, %edi
|
|
93
|
+
\tmovslq\t(%rcx,%rdi,4), %rax
|
|
94
|
+
\taddq\t%rcx, %rax
|
|
95
|
+
\tnotrack jmp\t*%rax
|
|
96
|
+
\t.section\t.rodata
|
|
97
|
+
\t.align 4
|
|
98
|
+
.L4:
|
|
99
|
+
\t.long\t.L8-.L4
|
|
100
|
+
\t.long\t.L7-.L4
|
|
101
|
+
\t.long\t.L6-.L4
|
|
102
|
+
\t.long\t.L5-.L4
|
|
103
|
+
\t.long\t.L3-.L4
|
|
104
|
+
\t.text
|
|
105
|
+
\t.p2align 4,,10
|
|
106
|
+
.L5:
|
|
107
|
+
\tmovl\t%esi, %eax
|
|
108
|
+
\txorl\t%edx, %eax
|
|
109
|
+
\tret
|
|
110
|
+
.L3:
|
|
111
|
+
\tmovl\t%esi, %eax
|
|
112
|
+
\torl\t%edx, %eax
|
|
113
|
+
\tret
|
|
114
|
+
.L8:
|
|
115
|
+
\tleal\t(%rsi,%rdx), %eax
|
|
116
|
+
\tret
|
|
117
|
+
.L7:
|
|
118
|
+
\tmovl\t%esi, %eax
|
|
119
|
+
\tsubl\t%edx, %eax
|
|
120
|
+
\tret
|
|
121
|
+
.L6:
|
|
122
|
+
\tmovl\t%esi, %eax
|
|
123
|
+
\timull\t%edx, %eax
|
|
124
|
+
\tret
|
|
125
|
+
\t.cfi_endproc
|
|
126
|
+
\t.size\tsel, .-sel
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
# Jump tables on other targets use plain (non-self-relative) label entries,
|
|
130
|
+
# plus stray inline constants; all are data directives, none are branches.
|
|
131
|
+
DATA_DIRECTIVES_ASM = """\
|
|
132
|
+
\t.type\ttbl, @function
|
|
133
|
+
tbl:
|
|
134
|
+
\t.cfi_startproc
|
|
135
|
+
\tjx\ta8
|
|
136
|
+
.Ltab:
|
|
137
|
+
\t.word\t.La
|
|
138
|
+
\t.word\t.Lb
|
|
139
|
+
\t.byte\t3
|
|
140
|
+
\t.quad\t0
|
|
141
|
+
.La:
|
|
142
|
+
\tadd.n\ta2, a2, a2
|
|
143
|
+
\tretw.n
|
|
144
|
+
.Lb:
|
|
145
|
+
\tretw.n
|
|
146
|
+
\t.size\ttbl, .-tbl
|
|
147
|
+
"""
|
|
148
|
+
|
|
77
149
|
|
|
78
150
|
class TestExtractFunctions(unittest.TestCase):
|
|
79
151
|
def test_gcc_functions_found(self):
|
|
@@ -173,6 +245,37 @@ class TestLoopSpans(unittest.TestCase):
|
|
|
173
245
|
self.assertEqual(asmdiff.loop_spans(lines), [])
|
|
174
246
|
|
|
175
247
|
|
|
248
|
+
class TestJumpTableData(unittest.TestCase):
|
|
249
|
+
"""Inline data (switch jump tables, constants) emitted inside a function
|
|
250
|
+
body is not counted as instructions and never reads as a loop span."""
|
|
251
|
+
|
|
252
|
+
def test_table_entries_stripped_from_body(self):
|
|
253
|
+
body = asmdiff.extract_functions(SWITCH_ASM)["sel"]
|
|
254
|
+
self.assertFalse(any(".long" in line for line in body))
|
|
255
|
+
self.assertIn(".L4:", body) # the table's anchor label is kept
|
|
256
|
+
|
|
257
|
+
def test_table_entries_not_counted_as_instructions(self):
|
|
258
|
+
body = asmdiff.extract_functions(SWITCH_ASM)["sel"]
|
|
259
|
+
insns, calls = asmdiff.analyze(body)
|
|
260
|
+
self.assertEqual(insns, 22) # 27 before the fix (5 .long entries)
|
|
261
|
+
self.assertEqual(calls, []) # notrack jmp *%rax is not a call
|
|
262
|
+
|
|
263
|
+
def test_self_relative_table_is_not_a_phantom_span(self):
|
|
264
|
+
# `.long .L5-.L4` references the table base .L4 from below; without
|
|
265
|
+
# stripping, that reads as a backward branch and invents a loop.
|
|
266
|
+
body = asmdiff.extract_functions(SWITCH_ASM)["sel"]
|
|
267
|
+
self.assertEqual(asmdiff.loop_spans(body), [])
|
|
268
|
+
|
|
269
|
+
def test_various_data_directives_stripped(self):
|
|
270
|
+
# .word/.byte/.quad jump tables and constants on other targets.
|
|
271
|
+
body = asmdiff.extract_functions(DATA_DIRECTIVES_ASM)["tbl"]
|
|
272
|
+
for directive in (".word", ".byte", ".quad"):
|
|
273
|
+
self.assertFalse(any(directive in line for line in body), directive)
|
|
274
|
+
insns, _ = asmdiff.analyze(body)
|
|
275
|
+
self.assertEqual(insns, 4) # 8 before the fix (4 data entries)
|
|
276
|
+
self.assertEqual(asmdiff.loop_spans(body), [])
|
|
277
|
+
|
|
278
|
+
|
|
176
279
|
class TestAutoPairs(unittest.TestCase):
|
|
177
280
|
def test_pairs_by_convention(self):
|
|
178
281
|
names = ["old_const", "new_const", "old_rt", "new_rt", "helper"]
|
|
@@ -244,6 +347,242 @@ class TestBuildMatrix(unittest.TestCase):
|
|
|
244
347
|
asmdiff.build_matrix([], ["bad"], cfg, "cfg.toml")
|
|
245
348
|
|
|
246
349
|
|
|
350
|
+
class TestIncludeFlags(unittest.TestCase):
|
|
351
|
+
"""Lifting include/define flags out of one recorded compile command."""
|
|
352
|
+
|
|
353
|
+
def test_glued_and_split_include_paths(self):
|
|
354
|
+
toks = ["cc", "-Iinc", "-I", "inc2", "-c", "a.c"]
|
|
355
|
+
self.assertEqual(asmdiff.include_flags(toks, "/build"),
|
|
356
|
+
["-I", "/build/inc", "-I", "/build/inc2"])
|
|
357
|
+
|
|
358
|
+
def test_absolute_paths_left_alone(self):
|
|
359
|
+
self.assertEqual(asmdiff.include_flags(["-I/abs/inc"], "/build"),
|
|
360
|
+
["-I", "/abs/inc"])
|
|
361
|
+
|
|
362
|
+
def test_defines_glued_and_split(self):
|
|
363
|
+
self.assertEqual(
|
|
364
|
+
asmdiff.include_flags(["-DFOO=1", "-D", "BAR", "-UNDEBUG"], "/b"),
|
|
365
|
+
["-DFOO=1", "-DBAR", "-UNDEBUG"])
|
|
366
|
+
|
|
367
|
+
def test_system_and_forced_include_families(self):
|
|
368
|
+
toks = ["-isystem", "sys", "-iquote", "q", "-idirafter", "d",
|
|
369
|
+
"-include", "cfg.h", "-imacros", "m.h"]
|
|
370
|
+
self.assertEqual(
|
|
371
|
+
asmdiff.include_flags(toks, "/build"),
|
|
372
|
+
["-isystem", "/build/sys", "-iquote", "/build/q",
|
|
373
|
+
"-idirafter", "/build/d", "-include", "/build/cfg.h",
|
|
374
|
+
"-imacros", "/build/m.h"])
|
|
375
|
+
|
|
376
|
+
def test_non_include_flags_and_source_dropped(self):
|
|
377
|
+
toks = ["gcc", "-O2", "-std=c11", "-Wall", "-g", "-c", "a.c",
|
|
378
|
+
"-o", "a.o", "-Iinc"]
|
|
379
|
+
self.assertEqual(asmdiff.include_flags(toks, "/b"), ["-I", "/b/inc"])
|
|
380
|
+
|
|
381
|
+
def test_dangling_flag_at_end_ignored(self):
|
|
382
|
+
self.assertEqual(asmdiff.include_flags(["-Iinc", "-I"], "/b"),
|
|
383
|
+
["-I", "/b/inc"])
|
|
384
|
+
|
|
385
|
+
def test_lowercase_isystem_not_split_as_capital_I(self):
|
|
386
|
+
# -isystem must not be read as -I + "system".
|
|
387
|
+
self.assertEqual(asmdiff.include_flags(["-isystem", "/s"], ""),
|
|
388
|
+
["-isystem", "/s"])
|
|
389
|
+
|
|
390
|
+
|
|
391
|
+
class TestCompileCommandsFlags(unittest.TestCase):
|
|
392
|
+
def _db(self, tmp, entries):
|
|
393
|
+
path = Path(tmp) / "compile_commands.json"
|
|
394
|
+
path.write_text(json.dumps(entries))
|
|
395
|
+
asmdiff._DB_CACHE.clear()
|
|
396
|
+
return str(path)
|
|
397
|
+
|
|
398
|
+
def test_matches_by_resolved_path_command_string(self):
|
|
399
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
400
|
+
src = Path(tmp) / "src" / "foo.c"
|
|
401
|
+
src.parent.mkdir()
|
|
402
|
+
src.touch()
|
|
403
|
+
db = self._db(tmp, [{
|
|
404
|
+
"directory": tmp, "file": str(src),
|
|
405
|
+
"command": f"cc -Iinc -DX=1 -c {src} -o foo.o"}])
|
|
406
|
+
self.assertEqual(asmdiff.compile_commands_flags(db, str(src)),
|
|
407
|
+
["-I", f"{tmp}/inc", "-DX=1"])
|
|
408
|
+
|
|
409
|
+
def test_matches_relative_file_and_arguments_array(self):
|
|
410
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
411
|
+
src = Path(tmp) / "foo.c"
|
|
412
|
+
src.touch()
|
|
413
|
+
db = self._db(tmp, [{
|
|
414
|
+
"directory": tmp, "file": "foo.c",
|
|
415
|
+
"arguments": ["cc", "-Iinc", "-c", "foo.c"]}])
|
|
416
|
+
self.assertEqual(asmdiff.compile_commands_flags(db, str(src)),
|
|
417
|
+
["-I", f"{tmp}/inc"])
|
|
418
|
+
|
|
419
|
+
def test_missing_source_errors(self):
|
|
420
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
421
|
+
db = self._db(tmp, [{"directory": tmp, "file": f"{tmp}/a.c",
|
|
422
|
+
"command": "cc -c a.c"}])
|
|
423
|
+
with self.assertRaises(SystemExit) as ctx:
|
|
424
|
+
asmdiff.compile_commands_flags(db, f"{tmp}/b.c")
|
|
425
|
+
self.assertIn("not found", str(ctx.exception))
|
|
426
|
+
|
|
427
|
+
def test_same_name_different_path_hint(self):
|
|
428
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
429
|
+
db = self._db(tmp, [{"directory": tmp,
|
|
430
|
+
"file": f"{tmp}/other/foo.c",
|
|
431
|
+
"command": "cc -c foo.c"}])
|
|
432
|
+
with self.assertRaises(SystemExit) as ctx:
|
|
433
|
+
asmdiff.compile_commands_flags(db, f"{tmp}/foo.c")
|
|
434
|
+
self.assertIn("different path", str(ctx.exception))
|
|
435
|
+
|
|
436
|
+
def test_bad_json_shape_errors(self):
|
|
437
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
438
|
+
path = Path(tmp) / "cc.json"
|
|
439
|
+
path.write_text('{"not": "a list"}')
|
|
440
|
+
asmdiff._DB_CACHE.clear()
|
|
441
|
+
with self.assertRaises(SystemExit):
|
|
442
|
+
asmdiff.compile_commands_flags(str(path), "x.c")
|
|
443
|
+
|
|
444
|
+
|
|
445
|
+
class TestTargetCompileCommands(unittest.TestCase):
|
|
446
|
+
def test_target_carries_expanded_db_path(self):
|
|
447
|
+
os.environ["ASMDIFF_TEST_DB"] = "/proj/build"
|
|
448
|
+
try:
|
|
449
|
+
cfg = {"t": {"cc": "gcc", "flags": ["-O2"],
|
|
450
|
+
"compile_commands": "$ASMDIFF_TEST_DB/cc.json"}}
|
|
451
|
+
matrix = asmdiff.build_matrix([], ["t"], cfg, "c")
|
|
452
|
+
self.assertEqual(matrix, ["gcc -O2"]) # str value unchanged
|
|
453
|
+
self.assertEqual(matrix[0].compile_commands,
|
|
454
|
+
"/proj/build/cc.json") # attribute carried
|
|
455
|
+
finally:
|
|
456
|
+
del os.environ["ASMDIFF_TEST_DB"]
|
|
457
|
+
|
|
458
|
+
def test_compile_commands_must_be_a_string(self):
|
|
459
|
+
cfg = {"t": {"cc": "gcc", "flags": [], "compile_commands": ["x"]}}
|
|
460
|
+
with self.assertRaises(SystemExit):
|
|
461
|
+
asmdiff.build_matrix([], ["t"], cfg, "c")
|
|
462
|
+
|
|
463
|
+
def test_cc_entries_have_no_db_attribute(self):
|
|
464
|
+
matrix = asmdiff.build_matrix(["gcc -O3"], [], None, None)
|
|
465
|
+
self.assertIsNone(getattr(matrix[0], "compile_commands", None))
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@contextlib.contextmanager
|
|
469
|
+
def _inside(directory):
|
|
470
|
+
"""Run a block with CWD set to ``directory`` (discovery is CWD-based)."""
|
|
471
|
+
prev = os.getcwd()
|
|
472
|
+
os.chdir(directory)
|
|
473
|
+
try:
|
|
474
|
+
yield
|
|
475
|
+
finally:
|
|
476
|
+
os.chdir(prev)
|
|
477
|
+
|
|
478
|
+
|
|
479
|
+
class TestFindCompileCommands(unittest.TestCase):
|
|
480
|
+
"""Auto-discovery of compile_commands.json near the current directory."""
|
|
481
|
+
|
|
482
|
+
def _touch_db(self, directory):
|
|
483
|
+
directory.mkdir(parents=True, exist_ok=True)
|
|
484
|
+
(directory / "compile_commands.json").write_text("[]")
|
|
485
|
+
|
|
486
|
+
def test_search_order_cwd_build_parent_parentbuild(self):
|
|
487
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
488
|
+
root = Path(tmp)
|
|
489
|
+
cwd = root / "sub"
|
|
490
|
+
cwd.mkdir()
|
|
491
|
+
for where, expect in [(root / "build", root / "build"),
|
|
492
|
+
(root, root),
|
|
493
|
+
(cwd / "build", cwd / "build"),
|
|
494
|
+
(cwd, cwd)]:
|
|
495
|
+
self._touch_db(where)
|
|
496
|
+
with _inside(cwd):
|
|
497
|
+
self.assertEqual(asmdiff.find_compile_commands(),
|
|
498
|
+
str(expect / "compile_commands.json"))
|
|
499
|
+
|
|
500
|
+
def test_nothing_found_returns_none(self):
|
|
501
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
502
|
+
cwd = Path(tmp) / "a" / "b"
|
|
503
|
+
cwd.mkdir(parents=True)
|
|
504
|
+
with _inside(cwd):
|
|
505
|
+
self.assertIsNone(asmdiff.find_compile_commands())
|
|
506
|
+
|
|
507
|
+
|
|
508
|
+
class TestDiscoveredCompileCommands(unittest.TestCase):
|
|
509
|
+
"""compile_commands = true in a target / bare --compile-commands."""
|
|
510
|
+
|
|
511
|
+
def test_target_true_discovers_and_is_soft(self):
|
|
512
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
513
|
+
db = Path(tmp) / "build" / "compile_commands.json"
|
|
514
|
+
db.parent.mkdir()
|
|
515
|
+
db.write_text("[]")
|
|
516
|
+
cfg = {"t": {"cc": "gcc", "compile_commands": True}}
|
|
517
|
+
with _inside(tmp):
|
|
518
|
+
matrix = asmdiff.build_matrix([], ["t"], cfg, "c")
|
|
519
|
+
self.assertEqual(matrix[0].compile_commands, str(db))
|
|
520
|
+
self.assertTrue(matrix[0].db_discovered)
|
|
521
|
+
|
|
522
|
+
def test_target_true_with_no_db_anywhere_errors(self):
|
|
523
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
524
|
+
cwd = Path(tmp) / "a" / "b"
|
|
525
|
+
cwd.mkdir(parents=True)
|
|
526
|
+
cfg = {"t": {"cc": "gcc", "compile_commands": True}}
|
|
527
|
+
with _inside(cwd), self.assertRaises(SystemExit) as ctx:
|
|
528
|
+
asmdiff.build_matrix([], ["t"], cfg, "c")
|
|
529
|
+
self.assertIn("compile_commands.json", str(ctx.exception))
|
|
530
|
+
|
|
531
|
+
def test_target_false_means_off(self):
|
|
532
|
+
cfg = {"t": {"cc": "gcc", "compile_commands": False}}
|
|
533
|
+
matrix = asmdiff.build_matrix([], ["t"], cfg, "c")
|
|
534
|
+
self.assertIsNone(matrix[0].compile_commands)
|
|
535
|
+
|
|
536
|
+
def test_explicit_path_stays_hard(self):
|
|
537
|
+
cfg = {"t": {"cc": "gcc", "compile_commands": "/x/cc.json"}}
|
|
538
|
+
matrix = asmdiff.build_matrix([], ["t"], cfg, "c")
|
|
539
|
+
self.assertEqual(matrix[0].compile_commands, "/x/cc.json")
|
|
540
|
+
self.assertFalse(matrix[0].db_discovered)
|
|
541
|
+
|
|
542
|
+
def test_cli_bare_flag_fills_cc_and_default_entries(self):
|
|
543
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
544
|
+
db = Path(tmp) / "compile_commands.json"
|
|
545
|
+
db.write_text("[]")
|
|
546
|
+
with _inside(tmp):
|
|
547
|
+
matrix = asmdiff.build_matrix(["gcc -O3"], [], None, None,
|
|
548
|
+
db_arg=True)
|
|
549
|
+
fallback = asmdiff.build_matrix([], [], None, None,
|
|
550
|
+
db_arg=True)
|
|
551
|
+
for entry in [matrix[0]] + list(fallback):
|
|
552
|
+
self.assertEqual(entry.compile_commands, str(db))
|
|
553
|
+
self.assertTrue(entry.db_discovered)
|
|
554
|
+
|
|
555
|
+
def test_cli_path_is_explicit_and_expanded(self):
|
|
556
|
+
os.environ["ASMDIFF_TEST_DB2"] = "/proj"
|
|
557
|
+
try:
|
|
558
|
+
matrix = asmdiff.build_matrix(["gcc -O3"], [], None, None,
|
|
559
|
+
db_arg="$ASMDIFF_TEST_DB2/cc.json")
|
|
560
|
+
finally:
|
|
561
|
+
del os.environ["ASMDIFF_TEST_DB2"]
|
|
562
|
+
self.assertEqual(matrix[0].compile_commands, "/proj/cc.json")
|
|
563
|
+
self.assertFalse(matrix[0].db_discovered)
|
|
564
|
+
|
|
565
|
+
def test_target_own_path_beats_cli(self):
|
|
566
|
+
cfg = {"t": {"cc": "gcc", "compile_commands": "/own/cc.json"}}
|
|
567
|
+
matrix = asmdiff.build_matrix([], ["t"], cfg, "c",
|
|
568
|
+
db_arg="/cli/cc.json")
|
|
569
|
+
self.assertEqual(matrix[0].compile_commands, "/own/cc.json")
|
|
570
|
+
|
|
571
|
+
def test_missing_source_soft_skips_with_note(self):
|
|
572
|
+
with tempfile.TemporaryDirectory() as tmp:
|
|
573
|
+
path = Path(tmp) / "compile_commands.json"
|
|
574
|
+
path.write_text(json.dumps([{"directory": tmp,
|
|
575
|
+
"file": f"{tmp}/a.c",
|
|
576
|
+
"command": "cc -Iinc -c a.c"}]))
|
|
577
|
+
asmdiff._DB_CACHE.clear()
|
|
578
|
+
err = io.StringIO()
|
|
579
|
+
with contextlib.redirect_stderr(err):
|
|
580
|
+
flags = asmdiff.compile_commands_flags(
|
|
581
|
+
str(path), f"{tmp}/b.c", missing_ok=True)
|
|
582
|
+
self.assertEqual(flags, [])
|
|
583
|
+
self.assertIn("b.c", err.getvalue())
|
|
584
|
+
|
|
585
|
+
|
|
247
586
|
class TestResolveCc(unittest.TestCase):
|
|
248
587
|
def test_home_and_env_vars_expand(self):
|
|
249
588
|
resolved = asmdiff.resolve_cc("~/bin/mycc", "t")
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
# asmdiff config — named compiler+flags targets.
|
|
2
|
-
#
|
|
3
|
-
# Copy to one of the locations asmdiff searches (first hit wins):
|
|
4
|
-
# 1. the path given with --config
|
|
5
|
-
# 2. asmdiff.toml next to the SOURCE.c being compiled
|
|
6
|
-
# 3. asmdiff.toml in the current directory
|
|
7
|
-
# 4. ~/.config/asmdiff.toml
|
|
8
|
-
#
|
|
9
|
-
# Each [table] is a target usable as `--target NAME`; the optional
|
|
10
|
-
# top-level `default` names the target(s) used when no --cc/--target
|
|
11
|
-
# is given. Compile at the flags your project ships with — that is
|
|
12
|
-
# the whole point of the tool.
|
|
13
|
-
|
|
14
|
-
default = "amy-host"
|
|
15
|
-
|
|
16
|
-
[amy-host]
|
|
17
|
-
cc = "gcc"
|
|
18
|
-
flags = [
|
|
19
|
-
"-O3", "-Wall", "-Wno-strict-aliasing", "-Wextra",
|
|
20
|
-
"-Wno-unused-parameter", "-Wpointer-arith", "-Wno-float-conversion",
|
|
21
|
-
"-Wno-missing-declarations", "-DAMY_WAVETABLE",
|
|
22
|
-
# For harnesses that #include "amy.h", point at the AMY sources:
|
|
23
|
-
# "-I/path/to/amy/src",
|
|
24
|
-
]
|
|
25
|
-
|
|
26
|
-
# cc values may use ~, $VARS, and glob patterns. A glob that matches
|
|
27
|
-
# several installed toolchains resolves to the highest version-sorted
|
|
28
|
-
# one (announced on stderr); pin the exact esp-NN directory instead if
|
|
29
|
-
# reproducibility across sessions matters more than convenience.
|
|
30
|
-
[s3-amy]
|
|
31
|
-
cc = "$HOME/.espressif/tools/xtensa-esp-elf/esp-*/xtensa-esp-elf/bin/xtensa-esp32s3-elf-gcc"
|
|
32
|
-
flags = [
|
|
33
|
-
"-O2", "-DAMY_USE_FIXEDPOINT", "-DNDEBUG",
|
|
34
|
-
"-Wno-strict-aliasing", "-Wno-float-conversion",
|
|
35
|
-
"-mlongcalls", "-Wpointer-arith",
|
|
36
|
-
]
|
|
File without changes
|
|
File without changes
|