asmdiff 0.1.1__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.1 → asmdiff-0.1.2}/PKG-INFO +96 -11
- {asmdiff-0.1.1 → asmdiff-0.1.2}/README.md +95 -10
- asmdiff-0.1.2/asmdiff.example.toml +50 -0
- {asmdiff-0.1.1 → asmdiff-0.1.2}/asmdiff.py +232 -15
- {asmdiff-0.1.1 → asmdiff-0.1.2}/pyproject.toml +2 -2
- {asmdiff-0.1.1 → asmdiff-0.1.2}/test_asmdiff.py +237 -0
- asmdiff-0.1.1/.claude/settings.local.json +0 -7
- asmdiff-0.1.1/asmdiff.example.toml +0 -36
- {asmdiff-0.1.1 → asmdiff-0.1.2}/LICENSE +0 -0
- {asmdiff-0.1.1 → 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,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: asmdiff
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.2
|
|
4
4
|
Summary: Compare per-function assembly between paired C implementations
|
|
5
5
|
Project-URL: Homepage, https://github.com/rt-rtos/asmdiff
|
|
6
6
|
Project-URL: Repository, https://github.com/rt-rtos/asmdiff
|
|
@@ -16,7 +16,7 @@ Classifier: Topic :: Software Development :: Debuggers
|
|
|
16
16
|
Requires-Python: >=3.8
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
|
|
19
|
-
# asmdiff
|
|
19
|
+
# [asmdiff](https://pypi.org/project/asmdiff/)
|
|
20
20
|
## per-function assembly comparison for paired C implementations
|
|
21
21
|
|
|
22
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.
|
|
@@ -94,7 +94,7 @@ $ asmdiff.py asmdiff_example.c
|
|
|
94
94
|
```
|
|
95
95
|
asmdiff.py SOURCE.c [SOURCE2.c] [--pair OLD:NEW]... [--across FUNC]...
|
|
96
96
|
[--cc 'CC FLAGS']... [--target NAME]... [--config PATH]
|
|
97
|
-
[-- EXTRA_FLAGS...]
|
|
97
|
+
[--compile-commands [PATH]] [-- EXTRA_FLAGS...]
|
|
98
98
|
```
|
|
99
99
|
|
|
100
100
|
| Option | Meaning |
|
|
@@ -105,6 +105,7 @@ asmdiff.py SOURCE.c [SOURCE2.c] [--pair OLD:NEW]... [--across FUNC]...
|
|
|
105
105
|
| `--cc 'CC FLAGS'` | One compiler invocation, command and flags in a single quoted string. Repeatable to build a matrix. |
|
|
106
106
|
| `--target NAME` | A named target from the config file, resolved to a `--cc` entry. Repeatable; appended to the matrix after `--cc` entries. |
|
|
107
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). |
|
|
108
109
|
| `-- FLAGS...` | Everything after a bare `--` is appended to *every* compiler invocation. |
|
|
109
110
|
|
|
110
111
|
With no `--cc` and no `--target`, the config file's top-level
|
|
@@ -139,19 +140,19 @@ each compiler+flags combination once:
|
|
|
139
140
|
|
|
140
141
|
```toml
|
|
141
142
|
# asmdiff.toml — next to your harnesses, in CWD, or in ~/.config/
|
|
142
|
-
default = "
|
|
143
|
+
default = "esp32s3" # target(s) used when no --cc/--target is given
|
|
143
144
|
|
|
144
|
-
[
|
|
145
|
+
[esp32s3] # production-like ESP32-S3 codegen
|
|
145
146
|
cc = "$HOME/.espressif/tools/xtensa-esp-elf/esp-*/xtensa-esp-elf/bin/xtensa-esp32s3-elf-gcc"
|
|
146
147
|
flags = [
|
|
147
|
-
"-O2", "-
|
|
148
|
+
"-O2", "-DMY_FEATURE", "-DNDEBUG",
|
|
148
149
|
"-Wno-strict-aliasing", "-mlongcalls",
|
|
149
|
-
"-I$HOME/project/components/
|
|
150
|
+
"-I$HOME/project/components/dsp/include",
|
|
150
151
|
]
|
|
151
152
|
|
|
152
|
-
[host
|
|
153
|
+
[host] # same defines on host gcc
|
|
153
154
|
cc = "gcc"
|
|
154
|
-
flags = ["-O2", "-
|
|
155
|
+
flags = ["-O2", "-DMY_FEATURE", "-I$HOME/project/components/dsp/include"]
|
|
155
156
|
```
|
|
156
157
|
|
|
157
158
|
`cc` values expand `~` and `$VARS` and may be glob patterns, so a config
|
|
@@ -168,8 +169,8 @@ shapes:
|
|
|
168
169
|
|
|
169
170
|
```bash
|
|
170
171
|
asmdiff.py h.c # config default target(s)
|
|
171
|
-
asmdiff.py h.c --target
|
|
172
|
-
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
|
|
173
174
|
```
|
|
174
175
|
|
|
175
176
|
A config placed next to your harness files travels with them: any invocation
|
|
@@ -179,6 +180,90 @@ included `asmdiff.example.toml` is a starting point. If a flag or include
|
|
|
179
180
|
path must vary per machine, that's what per-machine config files are for —
|
|
180
181
|
nothing lives in the tool.
|
|
181
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
|
+
|
|
182
267
|
## Whole-file summary
|
|
183
268
|
|
|
184
269
|
With no `--pair`, no `--across`, and no `old_*`/`new_*` functions to
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# asmdiff
|
|
1
|
+
# [asmdiff](https://pypi.org/project/asmdiff/)
|
|
2
2
|
## per-function assembly comparison for paired C implementations
|
|
3
3
|
|
|
4
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.
|
|
@@ -76,7 +76,7 @@ $ asmdiff.py asmdiff_example.c
|
|
|
76
76
|
```
|
|
77
77
|
asmdiff.py SOURCE.c [SOURCE2.c] [--pair OLD:NEW]... [--across FUNC]...
|
|
78
78
|
[--cc 'CC FLAGS']... [--target NAME]... [--config PATH]
|
|
79
|
-
[-- EXTRA_FLAGS...]
|
|
79
|
+
[--compile-commands [PATH]] [-- EXTRA_FLAGS...]
|
|
80
80
|
```
|
|
81
81
|
|
|
82
82
|
| Option | Meaning |
|
|
@@ -87,6 +87,7 @@ asmdiff.py SOURCE.c [SOURCE2.c] [--pair OLD:NEW]... [--across FUNC]...
|
|
|
87
87
|
| `--cc 'CC FLAGS'` | One compiler invocation, command and flags in a single quoted string. Repeatable to build a matrix. |
|
|
88
88
|
| `--target NAME` | A named target from the config file, resolved to a `--cc` entry. Repeatable; appended to the matrix after `--cc` entries. |
|
|
89
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). |
|
|
90
91
|
| `-- FLAGS...` | Everything after a bare `--` is appended to *every* compiler invocation. |
|
|
91
92
|
|
|
92
93
|
With no `--cc` and no `--target`, the config file's top-level
|
|
@@ -121,19 +122,19 @@ each compiler+flags combination once:
|
|
|
121
122
|
|
|
122
123
|
```toml
|
|
123
124
|
# asmdiff.toml — next to your harnesses, in CWD, or in ~/.config/
|
|
124
|
-
default = "
|
|
125
|
+
default = "esp32s3" # target(s) used when no --cc/--target is given
|
|
125
126
|
|
|
126
|
-
[
|
|
127
|
+
[esp32s3] # production-like ESP32-S3 codegen
|
|
127
128
|
cc = "$HOME/.espressif/tools/xtensa-esp-elf/esp-*/xtensa-esp-elf/bin/xtensa-esp32s3-elf-gcc"
|
|
128
129
|
flags = [
|
|
129
|
-
"-O2", "-
|
|
130
|
+
"-O2", "-DMY_FEATURE", "-DNDEBUG",
|
|
130
131
|
"-Wno-strict-aliasing", "-mlongcalls",
|
|
131
|
-
"-I$HOME/project/components/
|
|
132
|
+
"-I$HOME/project/components/dsp/include",
|
|
132
133
|
]
|
|
133
134
|
|
|
134
|
-
[host
|
|
135
|
+
[host] # same defines on host gcc
|
|
135
136
|
cc = "gcc"
|
|
136
|
-
flags = ["-O2", "-
|
|
137
|
+
flags = ["-O2", "-DMY_FEATURE", "-I$HOME/project/components/dsp/include"]
|
|
137
138
|
```
|
|
138
139
|
|
|
139
140
|
`cc` values expand `~` and `$VARS` and may be glob patterns, so a config
|
|
@@ -150,8 +151,8 @@ shapes:
|
|
|
150
151
|
|
|
151
152
|
```bash
|
|
152
153
|
asmdiff.py h.c # config default target(s)
|
|
153
|
-
asmdiff.py h.c --target
|
|
154
|
-
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
|
|
155
156
|
```
|
|
156
157
|
|
|
157
158
|
A config placed next to your harness files travels with them: any invocation
|
|
@@ -161,6 +162,90 @@ included `asmdiff.example.toml` is a starting point. If a flag or include
|
|
|
161
162
|
path must vary per machine, that's what per-machine config files are for —
|
|
162
163
|
nothing lives in the tool.
|
|
163
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
|
+
|
|
164
249
|
## Whole-file summary
|
|
165
250
|
|
|
166
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.
|
|
@@ -267,26 +443,52 @@ def target_command(config, name, config_path):
|
|
|
267
443
|
if isinstance(flags, str) or not all(isinstance(f, str) for f in flags):
|
|
268
444
|
sys.exit(f"error: target [{name}]: flags must be an array of strings")
|
|
269
445
|
flags = [os.path.expandvars(f) for f in flags]
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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):
|
|
274
462
|
"""Resolve the compiler matrix.
|
|
275
463
|
|
|
276
464
|
--cc strings verbatim, then --target entries, in that order. With
|
|
277
465
|
neither, the config's `default` (a target name or list of names);
|
|
278
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.
|
|
279
471
|
"""
|
|
280
472
|
entries = list(cc_args)
|
|
281
473
|
entries += [target_command(config, name, config_path)
|
|
282
474
|
for name in target_args]
|
|
283
|
-
if entries:
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
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
|
|
290
492
|
|
|
291
493
|
|
|
292
494
|
def asm_output_name(cc_cmd, harness):
|
|
@@ -305,7 +507,9 @@ def asm_output_name(cc_cmd, harness):
|
|
|
305
507
|
def compile_to_asm(cc_cmd, extra_flags, harness, out_dir):
|
|
306
508
|
"""Run one compiler to -S; return the asm text.
|
|
307
509
|
|
|
308
|
-
|
|
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.
|
|
309
513
|
Exits with the compiler's stderr on a compile failure.
|
|
310
514
|
"""
|
|
311
515
|
argv = shlex.split(cc_cmd)
|
|
@@ -313,8 +517,14 @@ def compile_to_asm(cc_cmd, extra_flags, harness, out_dir):
|
|
|
313
517
|
print(f"warning: {argv[0]} not found on PATH, skipping",
|
|
314
518
|
file=sys.stderr)
|
|
315
519
|
return None
|
|
316
|
-
|
|
317
|
-
|
|
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)])
|
|
318
528
|
proc = subprocess.run(cmd, capture_output=True, text=True)
|
|
319
529
|
if proc.returncode != 0:
|
|
320
530
|
sys.exit(f"error: compile failed: {' '.join(cmd)}\n{proc.stderr}")
|
|
@@ -549,6 +759,12 @@ def main(argv=None):
|
|
|
549
759
|
help="named [table] from the config file, resolved "
|
|
550
760
|
"to a --cc entry (repeatable; appended to the "
|
|
551
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")
|
|
552
768
|
parser.add_argument("--config", metavar="PATH",
|
|
553
769
|
help=f"config file; default search: {CONFIG_NAME} "
|
|
554
770
|
"next to SOURCE.c, in the current directory, "
|
|
@@ -564,7 +780,8 @@ def main(argv=None):
|
|
|
564
780
|
"use --across FUNC for two files")
|
|
565
781
|
config_path = find_config(args.config, args.sources)
|
|
566
782
|
config = load_config(config_path) if config_path else None
|
|
567
|
-
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)
|
|
568
785
|
if args.across and len(args.sources) == 1 and len(matrix) < 2:
|
|
569
786
|
parser.error("--across on one file needs at least two --cc entries")
|
|
570
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"
|
|
@@ -15,7 +15,7 @@ authors = [
|
|
|
15
15
|
]
|
|
16
16
|
keywords = [
|
|
17
17
|
"assembly", "compiler", "gcc", "clang", "disassembly", "codegen",
|
|
18
|
-
"diff", "static-analysis", "embedded", "firmware", "dsp",
|
|
18
|
+
"diff", "static-analysis", "embedded", "firmware", "dsp",
|
|
19
19
|
"xtensa", "arm", "riscv", "cli", "devtools", "loop-analysis", "cfg"
|
|
20
20
|
]
|
|
21
21
|
classifiers = [
|
|
@@ -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
|
|
@@ -346,6 +347,242 @@ class TestBuildMatrix(unittest.TestCase):
|
|
|
346
347
|
asmdiff.build_matrix([], ["bad"], cfg, "cfg.toml")
|
|
347
348
|
|
|
348
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
|
+
|
|
349
586
|
class TestResolveCc(unittest.TestCase):
|
|
350
587
|
def test_home_and_env_vars_expand(self):
|
|
351
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
|