Skip to content

Commit aa8ef0e

Browse files
hjmjohnsonjcfr
andcommitted
feat: Darwin-style framework search (-F, -iframework) with ordered path lookup
This change teaches simplecpp to resolve headers from Apple-style Framework directories while preserving the left-to-right order of interleaved -I/-F/-iframework search paths (like GCC/Clang on Darwin). This enables both: - `__has_include(<Pkg/Hdr.h>)` -> `<Pkg.framework/Headers/Hdr.h>` (or `PrivateHeaders`) - `#include <Pkg/Hdr.h>` -> same framework layout when a package prefix exists Changes: - Add `DUI::SearchPath` with `PathKind {Include, Framework, SystemFramework}`. - If `DUI::searchPaths` is non-empty, use it verbatim (interleaved -I/-F/-iframework). Otherwise preserve back-compat by mirroring `includePaths` as Include paths. - Update `openHeader()` to consult typed paths, and only rewrite `<Pkg/Hdr.h>` to `Pkg.framework/{Headers,PrivateHeaders}/Hdr.h` when a package prefix exists. - Implement `toAppleFrameworkRelatives()` returning prioritized candidates (Headers first, then PrivateHeaders). - Tests use `PathKind::Framework` when checking framework layout. CLI - Support -F<dir> and -iframework<dir> (keep -I as before). Behavior notes - The order of -I/-F/-iframework is preserved exactly as provided. - `Framework` vs `SystemFramework` differ only in diagnostic semantics (not lookup). - Legacy users who only set `DUI::includePaths` see identical behavior. Tests - Add `appleFrameworkHasIncludeTest` for `__has_include` resolution. - Add `appleFrameworkIncludeTest` for `#include` resolution. - Add dummy fixture: `testsuite/Foundation.framework/Headers/Foundation.h`. This brings simplecpp closer to GCC/Clang behavior on macOS and enables robust resolution of framework headers like `Foundation/Foundation.h`. Co-authored-by: Jean-Christophe Fillion-Robin <jchris.fillionr@kitware.com> Co-authored-by: Hans Johnson <hans-johnson@uiowa.edu> Suggested-by: glankk <glankk@users.noreply.github.com>
1 parent 9739f7e commit aa8ef0e

7 files changed

Lines changed: 481 additions & 36 deletions

File tree

integration_test.py

Lines changed: 246 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,79 @@
44
import pathlib
55
import platform
66
import pytest
7-
from testutils import simplecpp, format_include_path_arg, format_include
7+
from testutils import (
8+
simplecpp,
9+
format_include_path_arg,
10+
format_framework_path_arg,
11+
format_iframework_path_arg,
12+
format_include,
13+
)
14+
15+
def __test_create_header(dir, hdr_relpath, with_pragma_once=True, already_included_error_msg=None):
16+
"""
17+
Creates a C header file under `dir/hdr_relpath` with simple include guards.
18+
19+
The file contains:
20+
- optional `#pragma once` (when `with_pragma_once=True`)
21+
- a header guard derived from `hdr_relpath` (e.g. "test.h" -> TEST_H_INCLUDED)
22+
- optional `#error <already_included_error_msg>` if the guard is already defined
23+
- a dummy non-preprocessor declaration to force line emission
24+
25+
Return absolute path to the created header file.
26+
"""
27+
inc_guard = hdr_relpath.upper().replace(".", "_") # "test.h" -> "TEST_H"
28+
header_file = os.path.join(dir, hdr_relpath)
29+
os.makedirs(os.path.dirname(header_file), exist_ok=True)
30+
with open(header_file, 'wt') as f:
31+
f.write(f"""
32+
{"#pragma once" if with_pragma_once else ""}
33+
#ifndef {inc_guard}_INCLUDED
34+
#define {inc_guard}_INCLUDED
35+
#else
36+
{f"#error {already_included_error_msg}" if already_included_error_msg else ""}
37+
#endif
38+
int __force_line_emission; /* anything non-preprocessor */
39+
""")
40+
return header_file
41+
42+
def __test_create_source(dir, include, is_include_sys=False):
43+
"""
44+
Creates a minimal C source file that includes a single header.
45+
46+
The generated `<dir>/test.c` contains one `#include` directive.
47+
If `is_include_sys` is True, the include is written as `<...>`;
48+
otherwise it is written as `"..."`.
49+
50+
Returns absolute path to the created header file.
51+
"""
52+
src_file = os.path.join(dir, 'test.c')
53+
with open(src_file, 'wt') as f:
54+
f.write(f"""
55+
#include {format_include(include, is_include_sys)}
56+
""")
57+
return src_file
58+
59+
def __test_create_framework(dir, fw_name, hdr_relpath, content="", private=False):
60+
"""
61+
Creates a minimal Apple-style framework layout containing one header.
62+
63+
The generated structure is:
64+
`<dir>/<fw_name>.framework/{Headers|PrivateHeaders}/<hdr_relpath>`
65+
66+
The header file contains the given `content` followed by a dummy
67+
declaration to force line emission.
68+
69+
Returns absolute path to the created header file.
70+
"""
71+
fwdir = os.path.join(dir, f"{fw_name}.framework", "PrivateHeaders" if private else "Headers")
72+
header_file = os.path.join(fwdir, hdr_relpath)
73+
os.makedirs(os.path.dirname(header_file), exist_ok=True)
74+
with open(header_file, "wt", encoding="utf-8") as f:
75+
f.write(f"""
76+
{content}
77+
int __force_line_emission; /* anything non-preprocessor */
78+
""")
79+
return header_file
880

981
def __test_relative_header_create_header(dir, with_pragma_once=True):
1082
"""
@@ -18,18 +90,10 @@ def __test_relative_header_create_header(dir, with_pragma_once=True):
1890
- absolute path to the created header file
1991
- expected compiler error substring for duplicate inclusion
2092
"""
21-
header_file = os.path.join(dir, 'test.h')
22-
with open(header_file, 'wt') as f:
23-
f.write(f"""
24-
{"#pragma once" if with_pragma_once else ""}
25-
#ifndef TEST_H_INCLUDED
26-
#define TEST_H_INCLUDED
27-
#else
28-
#error header_was_already_included
29-
#endif
30-
const int dummy = 1;
31-
""")
32-
return header_file, "error: #error header_was_already_included"
93+
already_included_error_msg="header_was_already_included"
94+
header_file = __test_create_header(
95+
dir, "test.h", with_pragma_once=with_pragma_once, already_included_error_msg=already_included_error_msg)
96+
return header_file, f"error: #error {already_included_error_msg}"
3397

3498
def __test_relative_header_create_source(dir, include1, include2, is_include1_sys=False, is_include2_sys=False, inv=False):
3599
"""
@@ -258,6 +322,175 @@ def test_same_name_header(record_property, tmpdir):
258322
assert "OK" in stdout
259323
assert stderr == ""
260324

325+
@pytest.mark.parametrize("is_sys", (False, True))
326+
@pytest.mark.parametrize("is_iframework", (False, True))
327+
@pytest.mark.parametrize("is_private", (False, True))
328+
def test_framework_lookup(record_property, tmpdir, is_sys, is_iframework, is_private):
329+
# Arrange framework: <tmp>/FwRoot/MyKit.framework/(Headers|PrivateHeaders)/Component.h
330+
fw_root = os.path.join(tmpdir, "FwRoot")
331+
__test_create_framework(fw_root, "MyKit", "Component.h", private=is_private)
332+
333+
test_file = __test_create_source(tmpdir, "MyKit/Component.h", is_include_sys=is_sys)
334+
335+
args = [format_iframework_path_arg(fw_root) if is_iframework else format_framework_path_arg(fw_root), test_file]
336+
_, stdout, stderr = simplecpp(args, cwd=tmpdir)
337+
record_property("stdout", stdout)
338+
record_property("stderr", stderr)
339+
340+
assert stderr == ""
341+
relative = "PrivateHeaders" if is_private else "Headers"
342+
assert f'#line 3 "{pathlib.PurePath(tmpdir).as_posix()}/FwRoot/MyKit.framework/{relative}/Component.h"' in stdout
343+
344+
@pytest.mark.parametrize("is_sys", (False, True))
345+
@pytest.mark.parametrize(
346+
"order,expected",
347+
[
348+
# Note:
349+
# - `I1` / `F1` / `IFW1` point to distinct directories and contain `Component_1.h` (a decoy).
350+
# - `I` / `F` / `IFW` point to directories that contain `Component.h`, which the
351+
# translation unit (TU) includes via `#include "MyKit/Component.h"`.
352+
#
353+
# This makes the winning flag (-I, -F, or -iframework) uniquely identifiable
354+
# in the preprocessor `#line` output.
355+
356+
# Sanity checks
357+
(("I",), "I"),
358+
(("F",), "F"),
359+
(("IFW",), "IFW"),
360+
361+
# Includes (-I)
362+
(("I1", "I"), "I"),
363+
(("I", "I1"), "I"),
364+
# Includes (-I) duplicates
365+
(("I1", "I", "I1"), "I"),
366+
(("I", "I1", "I"), "I"),
367+
368+
# Framework (-F)
369+
(("F1", "F"), "F"),
370+
(("F", "F1"), "F"),
371+
# Framework (-F) duplicates
372+
(("F1", "F", "F1"), "F"),
373+
(("F", "F1", "F"), "F"),
374+
375+
# System framework (-iframework)
376+
(("IFW1", "IFW"), "IFW"),
377+
(("IFW", "IFW1"), "IFW"),
378+
# System framework (-iframework) duplicates
379+
(("IFW1", "IFW", "IFW1"), "IFW"),
380+
(("IFW", "IFW1", "IFW"), "IFW"),
381+
382+
# -I and -F are processed as specified (left-to-right)
383+
(("I", "F"), "I"),
384+
(("I1", "I", "F"), "I"),
385+
(("F", "I"), "F"),
386+
(("F1", "F", "I"), "F"),
387+
388+
# -I and -F beat system framework (-iframework)
389+
(("I", "IFW"), "I"),
390+
(("F", "IFW"), "F"),
391+
(("IFW", "F"), "F"),
392+
(("IFW", "I", "F"), "I"),
393+
(("IFW", "I1", "F1", "I", "F"), "I"),
394+
(("IFW", "I"), "I"),
395+
(("IFW", "F", "I"), "F"),
396+
(("IFW", "F1", "I1", "F", "I"), "F"),
397+
],
398+
)
399+
def test_searchpath_order(record_property, tmpdir, is_sys, order, expected):
400+
"""
401+
Validate include resolution order across -I (user include), -F (user framework),
402+
and -iframework (system framework) using a minimal file layout, asserting which
403+
physical header path appears in the preprocessor #line output.
404+
405+
The test constructs three parallel trees (two entries per kind):
406+
- inc{,_1}/MyKit/Component{,_1}.h # for -I
407+
- Fw{,_1}/MyKit.framework/Headers/Component{,_1}.h # for -F
408+
- SysFw{,_1}/MyKit.framework/Headers/Component{,_1}.h # for -iframework
409+
410+
It then preprocesses a TU that includes "MyKit/Component.h" (or <...> when
411+
is_sys=True), assembles compiler args in the exact `order`, and asserts that
412+
only the expected path appears in #line. Distinct names (Component.h vs
413+
Component_1.h) ensure a unique winner per bucket.
414+
415+
References:
416+
- https://gcc.gnu.org/onlinedocs/cpp/Invocation.html#Invocation
417+
- https://gcc.gnu.org/onlinedocs/gcc/Darwin-Options.html
418+
"""
419+
420+
# Create two include dirs, two user framework dirs, and two system framework dirs
421+
inc_dirs, fw_dirs, sysfw_dirs = [], [], []
422+
423+
def _suffix(idx: int) -> str:
424+
return f"_{idx}" if idx > 0 else ""
425+
426+
for idx in range(2):
427+
# -I paths
428+
inc_dir = os.path.join(tmpdir, f"inc{_suffix(idx)}")
429+
__test_create_header(inc_dir, hdr_relpath=f"MyKit/Component{_suffix(idx)}.h")
430+
inc_dirs.append(inc_dir)
431+
432+
# -F paths (user frameworks)
433+
fw_dir = os.path.join(tmpdir, f"Fw{_suffix(idx)}")
434+
__test_create_framework(fw_dir, "MyKit", f"Component{_suffix(idx)}.h")
435+
fw_dirs.append(fw_dir)
436+
437+
# -iframework paths (system frameworks)
438+
sysfw_dir = os.path.join(tmpdir, f"SysFw{_suffix(idx)}")
439+
__test_create_framework(sysfw_dir, "MyKit", f"Component{_suffix(idx)}.h")
440+
sysfw_dirs.append(sysfw_dir)
441+
442+
# Translation unit under test: include MyKit/Component.h (quote or system form)
443+
test_file = __test_create_source(tmpdir, "MyKit/Component.h", is_include_sys=is_sys)
444+
445+
def idx_from_flag(prefix: str, flag: str) -> int:
446+
"""Extract numeric suffix from tokens like 'I1', 'F1', 'IFW1'.
447+
Returns 0 when no suffix is present (e.g., 'I', 'F', 'IFW')."""
448+
return int(flag[len(prefix):]) if len(flag) > len(prefix) else 0
449+
450+
# Build argv in the exact order requested by `order`
451+
args = []
452+
for flag in order:
453+
if flag in ["I", "I1"]:
454+
args.append(format_include_path_arg(inc_dirs[idx_from_flag("I", flag)]))
455+
elif flag in ["F", "F1"]:
456+
args.append(format_framework_path_arg(fw_dirs[idx_from_flag("F", flag)]))
457+
elif flag in ["IFW", "IFW1"]:
458+
args.append(format_iframework_path_arg(sysfw_dirs[idx_from_flag("IFW", flag)]))
459+
else:
460+
raise AssertionError(f"unknown flag in order: {flag}")
461+
args.append(test_file)
462+
463+
# Run the preprocessor and capture outputs
464+
_, stdout, stderr = simplecpp(args, cwd=tmpdir)
465+
record_property("stdout", stdout)
466+
record_property("stderr", stderr)
467+
468+
# Resolve the absolute expected/forbidden paths we want to see in #line output
469+
root = pathlib.PurePath(tmpdir).as_posix()
470+
471+
inc_paths = [f"{root}/inc{_suffix(idx)}/MyKit/Component{_suffix(idx)}.h" for idx in range(2)]
472+
fw_paths = [f"{root}/Fw{_suffix(idx)}/MyKit.framework/Headers/Component{_suffix(idx)}.h" for idx in range(2)]
473+
ifw_paths = [f"{root}/SysFw{_suffix(idx)}/MyKit.framework/Headers/Component{_suffix(idx)}.h" for idx in range(2)]
474+
all_candidate_paths = [*inc_paths, *fw_paths, *ifw_paths]
475+
476+
# Compute the single path we expect to appear
477+
expected_path = None
478+
if expected in ["I", "I1"]:
479+
expected_path = inc_paths[idx_from_flag("I", expected)]
480+
elif expected in ["F", "F1"]:
481+
expected_path = fw_paths[idx_from_flag("F", expected)]
482+
elif expected in ["IFW", "IFW1"]:
483+
expected_path = ifw_paths[idx_from_flag("IFW", expected)]
484+
assert expected_path is not None, "test configuration error: expected token not recognized"
485+
486+
# Assert ONLY the expected path appears in the preprocessor #line output
487+
assert expected_path in stdout
488+
for p in (p for p in all_candidate_paths if p != expected_path):
489+
assert p not in stdout
490+
491+
# No diagnostics expected
492+
assert stderr == ""
493+
261494
def test_pragma_once_matching(record_property, tmpdir):
262495
test_dir = os.path.join(tmpdir, "test_dir")
263496
test_subdir = os.path.join(test_dir, "test_subdir")

main.cpp

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,13 @@ int main(int argc, char **argv)
4343
}
4444
case 'I': { // include path
4545
const char * const value = arg[2] ? (argv[i] + 2) : argv[++i];
46-
dui.includePaths.push_back(value);
46+
dui.searchPaths.push_back({value, simplecpp::DUI::PathKind::Include});
47+
found = true;
48+
break;
49+
}
50+
case 'F': { // framework path
51+
const char * const value = arg[2] ? (argv[i] + 2) : argv[++i];
52+
dui.searchPaths.push_back({value, simplecpp::DUI::PathKind::Framework});
4753
found = true;
4854
break;
4955
}
@@ -54,6 +60,9 @@ int main(int argc, char **argv)
5460
} else if (std::strncmp(arg, "-is",3)==0) {
5561
use_istream = true;
5662
found = true;
63+
} else if (std::strncmp(arg, "-iframework", 11) == 0) {
64+
dui.searchPaths.push_back({arg + 11, simplecpp::DUI::PathKind::SystemFramework});
65+
found = true;
5766
}
5867
break;
5968
case 's':
@@ -100,6 +109,8 @@ int main(int argc, char **argv)
100109
std::cout << "simplecpp [options] filename" << std::endl;
101110
std::cout << " -DNAME Define NAME." << std::endl;
102111
std::cout << " -IPATH Include path." << std::endl;
112+
std::cout << " -FPATH Framework path." << std::endl;
113+
std::cout << " -iframeworkPATH System framework path." << std::endl;
103114
std::cout << " -include=FILE Include FILE." << std::endl;
104115
std::cout << " -UNAME Undefine NAME." << std::endl;
105116
std::cout << " -std=STD Specify standard." << std::endl;

0 commit comments

Comments
 (0)