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.
@@ -0,0 +1,9 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(python3 test_asmdiff.py)",
5
+ "Bash(git -C /home/fatta/esp-idf/tools/asmdiff log --oneline -5)",
6
+ "Bash(git -C /home/fatta/esp-idf/tools/asmdiff diff main...HEAD --stat)"
7
+ ]
8
+ }
9
+ }
@@ -0,0 +1,2 @@
1
+ __pycache__/
2
+ *.py[cod]
@@ -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.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 = "s3-amy" # target(s) used when no --cc/--target is given
143
+ default = "esp32s3" # target(s) used when no --cc/--target is given
143
144
 
144
- [s3-amy] # production-like ESP32-S3 codegen
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", "-DAMY_USE_FIXEDPOINT", "-DNDEBUG",
148
+ "-O2", "-DMY_FEATURE", "-DNDEBUG",
148
149
  "-Wno-strict-aliasing", "-mlongcalls",
149
- "-I$HOME/project/components/amy/src",
150
+ "-I$HOME/project/components/dsp/include",
150
151
  ]
151
152
 
152
- [host-fixed] # same defines on host gcc
153
+ [host] # same defines on host gcc
153
154
  cc = "gcc"
154
- flags = ["-O2", "-DAMY_USE_FIXEDPOINT", "-I$HOME/amy/src"]
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 s3-amy --target host-fixed # two-target matrix
172
- asmdiff.py h.c --across f --target s3-amy --cc 'gcc -O2' # mix freely
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 = "s3-amy" # target(s) used when no --cc/--target is given
125
+ default = "esp32s3" # target(s) used when no --cc/--target is given
125
126
 
126
- [s3-amy] # production-like ESP32-S3 codegen
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", "-DAMY_USE_FIXEDPOINT", "-DNDEBUG",
130
+ "-O2", "-DMY_FEATURE", "-DNDEBUG",
130
131
  "-Wno-strict-aliasing", "-mlongcalls",
131
- "-I$HOME/project/components/amy/src",
132
+ "-I$HOME/project/components/dsp/include",
132
133
  ]
133
134
 
134
- [host-fixed] # same defines on host gcc
135
+ [host] # same defines on host gcc
135
136
  cc = "gcc"
136
- flags = ["-O2", "-DAMY_USE_FIXEDPOINT", "-I$HOME/amy/src"]
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 s3-amy --target host-fixed # two-target matrix
154
- asmdiff.py h.c --across f --target s3-amy --cc 'gcc -O2' # mix freely
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
- return shlex.join([resolve_cc(cc, name), *flags])
271
-
272
-
273
- def build_matrix(cc_args, target_args, config, config_path):
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
- return entries
285
- default = (config or {}).get("default")
286
- if default:
287
- names = [default] if isinstance(default, str) else list(default)
288
- return [target_command(config, name, config_path) for name in names]
289
- return [f"{cc} {FALLBACK_FLAGS}" for cc in DEFAULT_COMPILERS]
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
- Returns None (with a warning) if the compiler is not on PATH.
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
- out_s = Path(out_dir) / asm_output_name(cc_cmd, harness)
317
- cmd = argv + list(extra_flags) + ["-S", "-o", str(out_s), str(harness)]
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.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,7 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(python3 test_asmdiff.py)"
5
- ]
6
- }
7
- }
@@ -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