ceedling 1.2.1 → 1.2.2
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.
- checksums.yaml +4 -4
- data/GIT_COMMIT_SHA +1 -1
- data/README.md +11 -9
- data/Rakefile +2 -1
- data/assets/features/ceedling +1 -1
- data/assets/fixtures/test_source_file_duplicate_basenames/test_calc_typo.c +14 -0
- data/assets/fixtures/tests_with_duplicate_test_names/test_foo.c +21 -0
- data/bin/actionator.rb +6 -12
- data/bin/app_cfg.rb +14 -1
- data/bin/ceedling +13 -3
- data/bin/cli.rb +25 -6
- data/bin/cli_handler.rb +113 -73
- data/bin/cli_helper.rb +51 -78
- data/bin/installation_selector.rb +81 -0
- data/bin/launchinator.rb +69 -0
- data/bin/mixin_resolvinator.rb +26 -36
- data/bin/objects.yml +13 -0
- data/bin/path_validator.rb +25 -29
- data/bin/versionator.rb +47 -48
- data/ceedling.gemspec +19 -15
- data/docs/BreakingChanges.md +70 -1
- data/docs/Changelog.md +74 -3
- data/docs/KnownIssues.md +90 -4
- data/docs/ReleaseNotes.md +13 -0
- data/lib/ceedling/build_log_writer.rb +33 -0
- data/lib/ceedling/config/build_root_policy.rb +70 -0
- data/lib/ceedling/config/configurator.rb +14 -16
- data/lib/ceedling/config/configurator_builder.rb +58 -28
- data/lib/ceedling/config/configurator_paths_validator.rb +179 -0
- data/lib/ceedling/config/configurator_plugins.rb +1 -1
- data/lib/ceedling/config/configurator_setup.rb +15 -28
- data/lib/ceedling/config/configurator_validator.rb +1 -110
- data/lib/ceedling/defaults.rb +4 -4
- data/lib/ceedling/dependencies/README.md +18 -16
- data/lib/ceedling/dependencies/dependency_cache_store.rb +20 -3
- data/lib/ceedling/dependencies/dependency_hasher.rb +35 -1
- data/lib/ceedling/filename_extension.rb +7 -1
- data/lib/ceedling/filesystem/FILE_TREE.md +253 -0
- data/lib/ceedling/filesystem/PATH_HANDLING.md +304 -0
- data/lib/ceedling/filesystem/README.md +41 -0
- data/lib/ceedling/filesystem/build_subpath.rb +102 -0
- data/lib/ceedling/filesystem/destination_collision.rb +25 -0
- data/lib/ceedling/{file_finder.rb → filesystem/file_finder.rb} +159 -104
- data/lib/ceedling/{file_finder_helper.rb → filesystem/file_finder_helper.rb} +36 -31
- data/lib/ceedling/filesystem/file_path_collection_utils.rb +183 -0
- data/lib/ceedling/{file_path_utils.rb → filesystem/file_path_utils.rb} +108 -97
- data/lib/ceedling/{file_system_wrapper.rb → filesystem/file_system_wrapper.rb} +6 -5
- data/lib/ceedling/filesystem/file_tree.rb +241 -0
- data/lib/ceedling/{file_wrapper.rb → filesystem/file_wrapper.rb} +45 -38
- data/lib/ceedling/filesystem/path_exclusions.rb +30 -0
- data/lib/ceedling/{path_matcher.rb → filesystem/path_matcher.rb} +27 -23
- data/lib/ceedling/{path_mirror.rb → filesystem/path_mirror.rb} +19 -18
- data/lib/ceedling/filesystem/path_spellings.rb +55 -0
- data/lib/ceedling/filesystem/project_paths.rb +184 -0
- data/lib/ceedling/filesystem/working_dir.rb +40 -0
- data/lib/ceedling/generators/generator.rb +5 -6
- data/lib/ceedling/generators/generator_partials.rb +1 -1
- data/lib/ceedling/generators/generator_test_results_backtrace.rb +7 -7
- data/lib/ceedling/handoff.rb +121 -0
- data/lib/ceedling/includes/include_factory.rb +3 -3
- data/lib/ceedling/includes/include_pathinator.rb +67 -10
- data/lib/ceedling/includes/includes.rb +1 -1
- data/lib/ceedling/installation.rb +92 -0
- data/lib/ceedling/library_resolver.rb +99 -0
- data/lib/ceedling/objects.yml +23 -2
- data/lib/ceedling/partials/partializer.rb +4 -4
- data/lib/ceedling/partials/partializer_config.rb +3 -1
- data/lib/ceedling/plugins/report_log_writer_plugin.rb +3 -5
- data/lib/ceedling/preprocess/preprocessinator_file_assembler.rb +1 -1
- data/lib/ceedling/preprocess/preprocessinator_includes_handler.rb +1 -1
- data/lib/ceedling/preprocess/preprocessinator_line_marker_includes_extractor.rb +1 -1
- data/lib/ceedling/rake_app/rakefile_component_resolver.rb +10 -17
- data/lib/ceedling/rakefile.rb +15 -1
- data/lib/ceedling/rakefiles/base/tasks_base.rake +1 -1
- data/lib/ceedling/rakefiles/release/rules_release.rake +4 -7
- data/lib/ceedling/release_invoker/release_build_executor.rb +33 -44
- data/lib/ceedling/release_invoker/release_build_planner.rb +14 -6
- data/lib/ceedling/reportinator.rb +3 -4
- data/lib/ceedling/setupinator.rb +94 -90
- data/lib/ceedling/test_context_extractor.rb +5 -1
- data/lib/ceedling/test_invoker/PIPELINE.md +9 -7
- data/lib/ceedling/test_invoker/mock_locator.rb +60 -2
- data/lib/ceedling/test_invoker/partials_manager.rb +12 -0
- data/lib/ceedling/test_invoker/standin_headerinator.rb +22 -13
- data/lib/ceedling/test_invoker/test_build_executor.rb +24 -40
- data/lib/ceedling/test_invoker/test_build_planner.rb +37 -18
- data/lib/ceedling/test_invoker/test_build_setup.rb +17 -12
- data/lib/ceedling/test_invoker/test_build_validations.rb +16 -31
- data/lib/ceedling/test_invoker/test_file_preprocessinator.rb +1 -1
- data/lib/ceedling/test_invoker/test_invoker.rb +6 -1
- data/lib/ceedling/test_invoker/test_pipeline_helpers.rb +1 -1
- data/lib/ceedling/test_invoker/test_source_file_directive_resolver.rb +40 -20
- data/lib/ceedling/tool_executor.rb +9 -3
- data/lib/ceedling/yaml_wrapper.rb +3 -2
- data/lib/version.rb +1 -1
- data/plugins/bullseye/config/defaults_bullseye.rb +1 -1
- data/plugins/bullseye/lib/bullseye.rb +9 -7
- data/plugins/cppcheck/config/defaults_cppcheck.rb +1 -1
- data/plugins/dependencies/config/defaults.yml +6 -6
- data/plugins/dependencies/dependencies.rake +4 -1
- data/plugins/dependencies/lib/dependencies.rb +18 -8
- data/plugins/dependencies/lib/dependency_spec.rb +17 -5
- data/plugins/fff/lib/fff.rb +45 -39
- data/plugins/fff/spec/fff_cmock_wrapper_spec.rb +128 -0
- data/plugins/gcov/config/defaults_gcov.rb +1 -1
- data/plugins/gcov/lib/console_reportinator.rb +4 -18
- data/plugins/gcov/lib/gcov.rb +9 -2
- data/plugins/gcov/lib/gcovr_reportinator.rb +8 -40
- data/plugins/gcov/lib/gcovr_thresholds.rb +74 -0
- data/plugins/gcov/lib/reportgenerator_reportinator.rb +1 -1
- data/plugins/module_generator/lib/module_generator.rb +3 -3
- data/plugins/report_tests_teamcity_stdout/lib/report_tests_teamcity_stdout.rb +53 -80
- data/plugins/sanitize/lib/sanitize_records.rb +3 -6
- data/plugins/valgrind/lib/valgrind.rb +14 -7
- data/site-local/configuration/reference/cmock.html +2 -1
- data/site-local/configuration/reference/files.html +9 -3
- data/site-local/configuration/reference/libraries.html +21 -3
- data/site-local/configuration/reference/paths.html +29 -6
- data/site-local/configuration/reference/project.html +17 -7
- data/site-local/configuration/reference/release-build.html +2 -0
- data/site-local/configuration/reference/tools.html +9 -0
- data/site-local/configuration/which-ceedling.html +377 -17
- data/site-local/development/releases.html +5 -1
- data/site-local/development/workflow.html +48 -43
- data/site-local/getting-started/builds.html +2 -0
- data/site-local/getting-started/command-line.html +56 -4
- data/site-local/plugins/dependencies.html +6 -3
- data/site-local/plugins/fff.html +42 -11
- data/site-local/plugins/gcov/gcovr.html +15 -12
- data/site-local/plugins/gcov/setup.html +2 -1
- data/site-local/plugins/report-tests-raw-output-log.html +3 -1
- data/site-local/plugins/report-tests-teamcity-stdout.html +6 -3
- data/site-local/plugins/sanitize.html +7 -6
- data/site-local/plugins/valgrind.html +8 -7
- data/site-local/project/upgrade.html +3 -0
- data/site-local/reference/command-line.html +9 -2
- data/site-local/sitemap.xml.gz +0 -0
- data/site-local/snapshot/plugins/bullseye/config/defaults_bullseye.rb +1 -1
- data/site-local/snapshot/plugins/gcov/config/defaults_gcov.rb +1 -1
- data/site-local/testing-guide/conventions.html +53 -0
- data/spec/integration/actionator_file_operations_spec.rb +91 -1
- data/spec/integration/compile_commands_json_db_spec.rb +1 -1
- data/spec/integration/configurator_validation_spec.rb +1 -1
- data/spec/integration/dependencies/dependency_tracker_cache_hardening_spec.rb +26 -0
- data/spec/integration/dependencies/dependency_tracker_core_spec.rb +22 -0
- data/spec/integration/launcher_handoff_spec.rb +297 -0
- data/spec/integration/mixin_loading_spec.rb +1 -1
- data/spec/integration/module_generator_spec.rb +3 -3
- data/spec/integration/nested_test_artifacts_spec.rb +99 -0
- data/spec/integration/partials_module_resolution_spec.rb +7 -4
- data/spec/integration/path_intake_scenarios_spec.rb +101 -0
- data/spec/integration/path_scenarios_spec.rb +322 -0
- data/spec/integration/quote_include_isolator_spec.rb +1 -1
- data/spec/integration/report_build_warnings_log_spec.rb +1 -1
- data/spec/integration/report_tests_log_factory_spec.rb +1 -1
- data/spec/integration/test_executable_manifest_spec.rb +1 -1
- data/spec/integration/test_includes_extractinator_spec.rb +2 -1
- data/spec/integration/test_naming_spec.rb +198 -0
- data/spec/integration/test_source_file_checks_spec.rb +166 -0
- data/spec/integration/tool_validator_spec.rb +1 -1
- data/spec/integration/upgrade_docs_spec.rb +84 -0
- data/spec/integration/vendor_copy_transient_io_retry_spec.rb +74 -5
- data/spec/support/file_tree_helper.rb +88 -0
- data/spec/support/integration/configurator_integration_helper.rb +6 -6
- data/spec/support/integration/dependency_tracker_integration_helper.rb +1 -1
- data/spec/support/integration/partials_generation_integration_helper.rb +2 -2
- data/spec/support/integration/path_scenario_harness.rb +173 -0
- data/spec/support/integration/spec_integration_helper.rb +1 -1
- data/spec/support/integration/test_includes_harness.rb +13 -2
- data/spec/support/spec_helper.rb +2 -0
- data/spec/support/system/spec_system_helper.rb +7 -6
- data/spec/support/system/system_context.rb +21 -3
- data/spec/support/working_dir_tracking.rb +49 -0
- data/spec/system/build_output_mirroring_spec.rb +2 -2
- data/spec/system/conditional_mock_includes_spec.rb +1 -1
- data/spec/system/delta_builds_spec.rb +137 -11
- data/spec/system/gcov_deployment_spec.rb +1 -0
- data/spec/system/header_include_path_validation_spec.rb +4 -4
- data/spec/system/launcher_handoff_spec.rb +82 -0
- data/spec/system/mock_include_folders_spec.rb +91 -0
- data/spec/system/parent_directory_reference_spec.rb +5 -3
- data/spec/system/path_handling_probes_spec.rb +192 -0
- data/spec/system/preprocessing_parameterized_tests_spec.rb +1 -1
- data/spec/system/rake_release_task_path_disambiguation_spec.rb +6 -6
- data/spec/system/rake_test_task_path_disambiguation_spec.rb +85 -0
- data/spec/system/special_path_characters_spec.rb +86 -0
- data/spec/system/support/common_test_cases.rb +23 -21
- data/spec/system/support/gcov_common_test_cases.rb +37 -9
- data/spec/system/support/sanitize_common_test_cases.rb +2 -2
- data/spec/system/support/valgrind_common_test_cases.rb +12 -12
- data/spec/system/test_pipeline_stop_points_spec.rb +4 -4
- data/spec/system/test_runner_system_include_spec.rb +1 -1
- data/spec/system/test_source_file_directive_spec.rb +34 -5
- data/spec/units/bin/actionator_spec.rb +14 -17
- data/spec/units/bin/app_cfg_spec.rb +37 -0
- data/spec/units/bin/cli_handler_spec.rb +282 -6
- data/spec/units/bin/cli_helper_spec.rb +116 -45
- data/spec/units/bin/installation_selector_spec.rb +114 -0
- data/spec/units/bin/launchinator_spec.rb +171 -0
- data/spec/units/bin/path_validator_spec.rb +10 -0
- data/spec/units/bin/versionator_spec.rb +38 -6
- data/spec/units/build_log_writer_spec.rb +57 -0
- data/spec/units/config/build_root_policy_spec.rb +61 -0
- data/spec/units/config/configurator_builder_spec.rb +37 -6
- data/spec/units/config/configurator_paths_validator_spec.rb +292 -0
- data/spec/units/config/configurator_setup_spec.rb +9 -65
- data/spec/units/config/configurator_spec.rb +27 -5
- data/spec/units/config/configurator_validator_spec.rb +1 -172
- data/spec/units/default_tools_quoting_spec.rb +83 -0
- data/spec/units/dependencies/dependency_cache_store_spec.rb +33 -2
- data/spec/units/dependencies/dependency_debug_tree_spec.rb +1 -1
- data/spec/units/dependencies/dependency_hasher_spec.rb +42 -1
- data/spec/units/dependencies/dependency_path_normalizer_spec.rb +1 -1
- data/spec/units/dependencies/dependency_tracker_spec.rb +3 -1
- data/spec/units/filename_extension_spec.rb +11 -0
- data/spec/units/filesystem/build_subpath_spec.rb +122 -0
- data/spec/units/filesystem/destination_collision_spec.rb +30 -0
- data/spec/units/{file_finder_helper_spec.rb → filesystem/file_finder_helper_spec.rb} +46 -1
- data/spec/units/{file_finder_spec.rb → filesystem/file_finder_spec.rb} +188 -22
- data/spec/units/{file_path_collection_utils_spec.rb → filesystem/file_path_collection_utils_spec.rb} +181 -1
- data/spec/units/{file_path_utils_spec.rb → filesystem/file_path_utils_spec.rb} +204 -87
- data/spec/units/filesystem/file_system_wrapper_spec.rb +33 -0
- data/spec/units/filesystem/file_tree_spec.rb +511 -0
- data/spec/units/filesystem/file_tree_windows_spec.rb +78 -0
- data/spec/units/{file_wrapper_spec.rb → filesystem/file_wrapper_spec.rb} +95 -12
- data/spec/units/{path_matcher_spec.rb → filesystem/path_matcher_spec.rb} +12 -1
- data/spec/units/{path_mirror_spec.rb → filesystem/path_mirror_spec.rb} +32 -4
- data/spec/units/filesystem/project_paths_spec.rb +147 -0
- data/spec/units/filesystem/working_dir_spec.rb +71 -0
- data/spec/units/generators/generator_spec.rb +12 -6
- data/spec/units/generators/generator_test_results_backtrace_spec.rb +50 -46
- data/spec/units/generators/generator_test_results_spec.rb +1 -1
- data/spec/units/handoff_spec.rb +205 -0
- data/spec/units/includes/include_factory_spec.rb +3 -3
- data/spec/units/includes/include_pathinator_spec.rb +46 -0
- data/spec/units/installation_spec.rb +228 -0
- data/spec/units/library_resolver_spec.rb +157 -0
- data/spec/units/mock_locator_spec.rb +76 -1
- data/spec/units/partials/partializer_config_spec.rb +7 -0
- data/spec/units/partials/partializer_spec.rb +1 -0
- data/spec/units/partials_manager_spec.rb +17 -0
- data/spec/units/plugins/bullseye_spec.rb +15 -2
- data/spec/units/plugins/console_reportinator_spec.rb +18 -20
- data/spec/units/plugins/dependencies_spec.rb +33 -7
- data/spec/units/plugins/dependency_spec_spec.rb +17 -0
- data/spec/units/plugins/gcov_spec.rb +28 -3
- data/spec/units/plugins/gcovr_reportinator_spec.rb +19 -47
- data/spec/units/plugins/gcovr_thresholds_spec.rb +95 -0
- data/spec/units/plugins/module_generator_spec.rb +5 -5
- data/spec/units/plugins/report_build_warnings_log_spec.rb +3 -3
- data/spec/units/plugins/report_log_writer_plugin_spec.rb +10 -1
- data/spec/units/plugins/report_tests_raw_output_log_spec.rb +13 -2
- data/spec/units/plugins/report_tests_teamcity_stdout_spec.rb +77 -3
- data/spec/units/plugins/reportgenerator_reportinator_spec.rb +1 -1
- data/spec/units/plugins/sanitize_records_spec.rb +3 -1
- data/spec/units/plugins/sanitize_spec.rb +1 -1
- data/spec/units/plugins/valgrind_spec.rb +23 -8
- data/spec/units/preprocess/preprocessinator_comment_stripper_spec.rb +1 -1
- data/spec/units/rake_app/rakefile_component_resolver_spec.rb +21 -0
- data/spec/units/release_build_executor_spec.rb +53 -14
- data/spec/units/release_build_planner_spec.rb +15 -3
- data/spec/units/reportinator_spec.rb +5 -5
- data/spec/units/standin_headerinator_spec.rb +31 -1
- data/spec/units/test_build_executor_spec.rb +65 -3
- data/spec/units/test_build_planner_spec.rb +104 -2
- data/spec/units/test_build_setup_spec.rb +96 -12
- data/spec/units/test_context_extractor_spec.rb +25 -0
- data/spec/units/test_file_preprocessinator_spec.rb +17 -1
- data/spec/units/test_includes_extractinator_spec.rb +5 -1
- data/spec/units/test_invoker/objects_wiring_spec.rb +6 -3
- data/spec/units/test_invoker_spec.rb +14 -1
- data/spec/units/test_source_file_directive_resolver_spec.rb +38 -7
- data/spec/units/tool_executor_spec.rb +26 -0
- data/spec/units/yaml_wrapper_spec.rb +11 -0
- metadata +109 -25
- data/lib/ceedling/file_path_collection_utils.rb +0 -164
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
# Path Handling
|
|
2
|
+
|
|
3
|
+
Ceedling spells every path one way, compares places by whole directories, and places every build file by its source's path from the project root. Three guarantees follow. No build file lands outside the build root. No two inputs share one build file. A build file's own path leads back to exactly one input.
|
|
4
|
+
|
|
5
|
+
This document is for developers and coding agents changing code that reads, compares, collects, or places paths. It begins with orientation and ends with detail. The orientation explains why paths need one spelling, the rules that govern them, the modules involved, and the invariants a change must keep. The later sections trace each rule to its methods, data structures, and specs.
|
|
6
|
+
|
|
7
|
+
## Orientation
|
|
8
|
+
|
|
9
|
+
### Why paths need one spelling
|
|
10
|
+
|
|
11
|
+
A project names one place in many spellings. A `:paths` entry may read `src/**`, `./src/**`, `../app/src/**`, `~/sdk/include`, `C:\Keil_v5\Include`, or a path through a symlink. Collection returns files relative to the working directory. Some lookups use absolute paths.
|
|
12
|
+
|
|
13
|
+
Comparing these spellings as text gives wrong answers in two ways. Two spellings of one place compare unequal. A text prefix also suggests containment where there is none. The text `..` begins `../../../Keil_v5/Include`, yet that directory lies far outside `..`.
|
|
14
|
+
|
|
15
|
+
Placement poses a second challenge. Same-named sources live in many directories. Some lie outside the project or on another drive. Each build file needs a place that belongs to its source alone and still lies inside the build root.
|
|
16
|
+
|
|
17
|
+
### The rules
|
|
18
|
+
|
|
19
|
+
Five rules govern path handling. Each later section expands one of them.
|
|
20
|
+
|
|
21
|
+
1. **One spelling.** Intake spells each path once. The spelling is relative to the working directory, with `~` expanded and the symlinks of its existing part resolved. A path with no relative form stays absolute. `ProjectPaths.canonical` produces it.
|
|
22
|
+
2. **Containment by directory.** A path lies within a root only when the root is one of its whole ancestor directories. `ProjectPaths.contains?` and `ProjectPaths.subdir_below` answer this. No code asks whether one path's text starts with another's.
|
|
23
|
+
3. **Placement by project path.** A build file sits below its build output root at its source's own directory path from the project root. `BuildSubpath.encode` writes that path so it never climbs or goes absolute. `..` becomes `_up_`, another drive goes under `_abs_` or `_unc_`, and Ceedling's own installed files go under `_ceedling_`.
|
|
24
|
+
4. **Exact inversion.** `BuildSubpath.decode` turns a build subpath back into the source's path. A lookup from a build file therefore finds its source by exact path before it ever falls back to matching by name.
|
|
25
|
+
5. **Collection leaves out the build.** Collection never returns the directories Ceedling creates. A glob based above the build root returns nothing from it. Configuration refuses a build root that holds the project.
|
|
26
|
+
|
|
27
|
+
### The project root
|
|
28
|
+
|
|
29
|
+
The project root is the working directory. It is where `ceedling` runs, and every relative path in configuration resolves against it. Mixins also search the project file's directory, but resolved paths are still relative to the working directory. `ProjectPaths.working_dir` is that directory with its symlinks resolved.
|
|
30
|
+
|
|
31
|
+
`WorkingDir` reads the working directory once and remembers it. A build resolves paths against it tens of thousands of times, and each read from the operating system costs tens of microseconds on macOS. Ceedling code changes directory only through `WorkingDir.chdir`, which forgets the remembered directory as it enters its block and again as it leaves. Paths resolved inside the block resolve against the new directory, and paths resolved after it against the old one. The Dependencies plugin's commands and `FileSystemWrapper#cd` go through it. A plain `Dir.chdir` leaves the remembered directory unchanged. The spec suite's `spec/support/working_dir_tracking.rb` forgets it on every `Dir.chdir`, because specs change directory in-process.
|
|
32
|
+
|
|
33
|
+
### Code organization
|
|
34
|
+
|
|
35
|
+
The path modules sit in `lib/ceedling/filesystem/`, whose [README](README.md) lists every file there. Most are pure module functions with no DIY dependency, so the launcher in `bin/` can use them too. Paths below are relative to that directory.
|
|
36
|
+
|
|
37
|
+
| File | Module or class | Role |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| `project_paths.rb` | `ProjectPaths` | Spelling, absolute and relative forms, containment |
|
|
40
|
+
| `working_dir.rb` | `WorkingDir` | The working directory, read once, and the one way to change it |
|
|
41
|
+
| `build_subpath.rb` | `BuildSubpath` | Encoding a directory into a build subpath and back |
|
|
42
|
+
| `path_spellings.rb` | `PathSpellings` | A place's listed and real spellings, and distances between places |
|
|
43
|
+
| `path_exclusions.rb` | `PathExclusions` | The three kinds of exclusion collection applies |
|
|
44
|
+
| `path_mirror.rb` | `PathMirror` | A file's subdirectory below the most specific containing root |
|
|
45
|
+
| `path_matcher.rb` | `PathMatcher` | Resolving `..` in a query, and matching trailing segments |
|
|
46
|
+
| `destination_collision.rb` | `DestinationCollision` | Stopping a build when two inputs map to one output |
|
|
47
|
+
| `file_path_collection_utils.rb` | `FilePathCollectionUtils` | Collecting `:paths` directories and `:files` revisions |
|
|
48
|
+
| `file_path_utils.rb` | `FilePathUtils` | Test names and every build file's path |
|
|
49
|
+
| `file_finder.rb` | `FileFinder` | Finding a build file's input from the build file's path |
|
|
50
|
+
| `../config/build_root_policy.rb` | `BuildRootPolicy` | What a build root may be, and what Ceedling creates in it |
|
|
51
|
+
| `../config/configurator_builder.rb` | `ConfiguratorBuilder` | Building collections with the exclusions applied |
|
|
52
|
+
| `../test_invoker/mock_locator.rb` | `MockLocator` | Where a mock is generated and how its test reaches it |
|
|
53
|
+
|
|
54
|
+
### Invariants
|
|
55
|
+
|
|
56
|
+
Every change to path code must keep these invariants. Specs pin each one.
|
|
57
|
+
|
|
58
|
+
- A build file lies below the build root. No subpath holds `..` or begins at a filesystem root.
|
|
59
|
+
- Two distinct inputs never share one output. `DestinationCollision` stops the build when they would.
|
|
60
|
+
- `BuildSubpath.decode( BuildSubpath.encode( dir ) )` returns `dir` in intake's spelling.
|
|
61
|
+
- A collection holds each place once, however many entries or spellings reach it.
|
|
62
|
+
- `ProjectPaths` and `BuildSubpath` never depend on DIY objects or configuration.
|
|
63
|
+
- Comparison folds case on Windows only. Stored spellings never change case.
|
|
64
|
+
|
|
65
|
+
## Spelling paths: `ProjectPaths`
|
|
66
|
+
|
|
67
|
+
`ProjectPaths` gives every path one spelling and answers containment between spellings. Its functions are pure over strings except where noted. The ones that read the disk say so.
|
|
68
|
+
|
|
69
|
+
### Functions
|
|
70
|
+
|
|
71
|
+
| Function | Result | Reads disk |
|
|
72
|
+
| --- | --- | --- |
|
|
73
|
+
| `working_dir` | `Dir.pwd` with symlinks resolved, remembered by `WorkingDir` | Once |
|
|
74
|
+
| `standardize(path)` | Trimmed, `/`-separated, no trailing separator, `~` expanded, `+:`/`-:` decorator kept | No |
|
|
75
|
+
| `absolute(path)` | Absolute, with `.` and `..` resolved against `working_dir`, drive letter upper case | No |
|
|
76
|
+
| `real_absolute(path)` | `absolute` with the symlinks of its longest existing prefix resolved | Yes |
|
|
77
|
+
| `canonical(path)` | `relative( real_absolute( path ) )`, the intake spelling | Yes |
|
|
78
|
+
| `relative(absolute)` | Relative to `working_dir`, `.` for the directory itself, absolute when no relative form exists | No |
|
|
79
|
+
| `contains?(root, path)` | Whether `path` is `root` or lies below it | No |
|
|
80
|
+
| `subdir_below(root, path)` | The directories from `root` down to `path`, `''` for `root` itself, nil for outside | No |
|
|
81
|
+
| `segments(path)` | Segments split on either separator, `.` and empty segments dropped | No |
|
|
82
|
+
| `same?(a, b)`, `starts_with?(path, prefix)` | Text comparison that folds case on Windows | No |
|
|
83
|
+
|
|
84
|
+
### Standardizing
|
|
85
|
+
|
|
86
|
+
`standardize` is the first step for any path read from configuration or the command line. It trims whitespace and converts backslashes. It drops trailing separators but keeps a bare root such as `/` or `C:/`. It expands `~` and `~/…` to `Dir.home`, also after a `+:` or `-:` decorator. A `~user` path stays literal and names a directory called `~user`. `FilePathUtils.standardize_in_place` applies it across configuration. `bin/path_validator.rb` applies it in the launcher.
|
|
87
|
+
|
|
88
|
+
### Absolute form and its memo
|
|
89
|
+
|
|
90
|
+
`absolute` resolves a path without touching the disk. A path that is already clean and absolute returns unchanged through `clean_absolute?`. Any other result is memoized per `[working_dir, Dir.home, path]` and frozen. A build asks for the same few roots thousands of times, so the memo keeps placement and lookup cheap. A `~user` path is prefixed with `./` before `File.expand_path`, so Ruby never reads it as a user's home.
|
|
91
|
+
|
|
92
|
+
### Canonical form
|
|
93
|
+
|
|
94
|
+
`canonical` is the spelling intake records. It resolves symlinks only along the longest existing prefix, through the private `real`. A path that does not exist yet keeps its missing tail as written. Collection resolves each entry's base this way. It never resolves every directory a glob yields, since an `#include` reaches a directory by its listed spelling.
|
|
95
|
+
|
|
96
|
+
### Containment
|
|
97
|
+
|
|
98
|
+
`subdir_below` compares absolute forms at a `/` boundary. `src` therefore never contains `src2`. The subdirectory it returns never holds `..` and is never absolute. `PathMirror.containing_subdir` builds on it and picks the shortest subdirectory, which is the most specific containing root.
|
|
99
|
+
|
|
100
|
+
## Placing build files: `BuildSubpath`
|
|
101
|
+
|
|
102
|
+
`BuildSubpath` turns a source's directory into the subpath its build files take below a build output root. The subpath is the directory's path from the project root, rewritten so it never climbs and never goes absolute.
|
|
103
|
+
|
|
104
|
+
### Markers
|
|
105
|
+
|
|
106
|
+
Four marker segments stand for what a relative path cannot hold.
|
|
107
|
+
|
|
108
|
+
| Marker | Stands for | Example directory | Encoded subpath |
|
|
109
|
+
| --- | --- | --- | --- |
|
|
110
|
+
| none | A directory inside the project | `src/drv` | `src/drv` |
|
|
111
|
+
| `_up_` | One `..` segment | `../shared/src` | `_up_/shared/src` |
|
|
112
|
+
| `_abs_` | A path with no relative form, on another Windows drive | `D:/sdk/inc` | `_abs_/D/sdk/inc` |
|
|
113
|
+
| `_unc_` | A Windows UNC share | `//server/share/inc` | `_unc_/server/share/inc` |
|
|
114
|
+
| `_ceedling_` | A file Ceedling ships, from the running installation, outside the project | `<installation>/plugins/fff/src` | `_ceedling_/plugins/fff/src` |
|
|
115
|
+
|
|
116
|
+
The project root itself encodes as `''`. Its files sit directly below the output root.
|
|
117
|
+
|
|
118
|
+
### Choosing a marker
|
|
119
|
+
|
|
120
|
+
`encode(path)` decides in a fixed order. It first asks whether the path lies outside the working directory by containment. If so and the path lies in one of `INSTALLED_DIRS`, the file is Ceedling's own and goes under `_ceedling_`. `INSTALLED_DIRS` are the `assets`, `bin`, `lib`, `plugins`, and `vendor` directories of `INSTALLATION_ROOT`, the directory above `lib/`. If the relative form is still absolute, the path takes `_abs_` or `_unc_`. Otherwise each `..` becomes `_up_`.
|
|
121
|
+
|
|
122
|
+
`_ceedling_` keeps Ceedling's own files short and stable. A gem installation can sit many directories away from a project. Without the marker its plugin sources would encode as a long chain of `_up_` segments that changes with every install location. A Ceedling copy vendored inside the project lies within the working directory, so it encodes by its ordinary path. A project may also sit inside a Ceedling checkout, as the system specs' projects do. Its sibling directories lie outside `INSTALLED_DIRS`, so they encode by `_up_` like any other.
|
|
123
|
+
|
|
124
|
+
### Escaping a marker-named directory
|
|
125
|
+
|
|
126
|
+
A real directory named like a marker gains one leading `_`. `MARKER_NAMED` matches `_up_`, `_abs_`, `_unc_`, and `_ceedling_`, already led by any number of `_`, in any case. So `src/_up_/x` encodes as `src/__up_/x`, and `src/__abs_` as `src/___abs_`. `decode` strips one leading `_` from a marker-named segment that begins with `__`. The match ignores case because macOS and Windows file systems fold case, so `_UP_` and `_up_` can name one directory there.
|
|
127
|
+
|
|
128
|
+
### Decoding
|
|
129
|
+
|
|
130
|
+
`decode(subpath)` inverts `encode` exactly. A leading `_ceedling_` joins the rest to `INSTALLATION_ROOT` and returns it relative to the working directory. A leading `_unc_` rebuilds `//server/share/…`. A leading `_abs_` rebuilds `D:/…` from its drive segment. Any other subpath maps each `_up_` back to `..` and unescapes the rest.
|
|
131
|
+
|
|
132
|
+
### Memo
|
|
133
|
+
|
|
134
|
+
`of_directory(dir)` memoizes `encode` per working directory and directory behind a Mutex. A build places many files from each directory, and build threads share the memo.
|
|
135
|
+
|
|
136
|
+
## Placement rules: `FilePathUtils`
|
|
137
|
+
|
|
138
|
+
`FilePathUtils#form_build_subpath(file, generated_roots:)` is the one placement rule. Every object, preprocessed file, dependencies file, and list file takes its subpath from it.
|
|
139
|
+
|
|
140
|
+
### The rule
|
|
141
|
+
|
|
142
|
+
The rule has three cases, tried in order.
|
|
143
|
+
|
|
144
|
+
1. A file below one of the caller's `generated_roots` mirrors below that root. These roots are the test's own mock and Partials directories, from `generated_roots(test)`. A mock's object therefore keeps the mock's own subdirectory.
|
|
145
|
+
2. A file elsewhere below the build root lands flat. These are vendored framework sources and runners, which never share a basename within one test.
|
|
146
|
+
3. Any other file lands at `BuildSubpath.of_directory( File.dirname( file ) )`.
|
|
147
|
+
|
|
148
|
+
In test `test/drivers/test_uart`, the source `src/drivers/uart.c` builds to `build/test/out/test/drivers/test_uart/src/drivers/uart.o`. A release builds it to `build/release/out/src/drivers/uart.o`. The source `../shared/crc.c` builds to `…/_up_/shared/crc.o`.
|
|
149
|
+
|
|
150
|
+
### Forming paths
|
|
151
|
+
|
|
152
|
+
`build_filepath(file, root:, ext:, generated_roots:)` joins the output root, the subpath, and the basename with the new extension. It checks each formed path's length once through `FileWrapper#check_path_length`. `@formed_paths`, a Set behind `@formed_lock`, remembers which paths were checked.
|
|
153
|
+
|
|
154
|
+
`mirror_build_objects(files, root:, ext:, generated_roots:)` maps a list of sources at once. It first drops duplicate sources by absolute path, since one file can reach a list under two spellings. It then calls `validate_distinct_build_files!`. That method uses `DestinationCollision.find` and `DestinationCollision.raise_for` to stop the build when two sources would build to one file. The message reads "`a` and `b` would build to the same file `x`. Rename one, or move it to its own directory."
|
|
155
|
+
|
|
156
|
+
These callers use the rule.
|
|
157
|
+
|
|
158
|
+
- `form_test_object_filepath` and `form_test_build_objects_filelist` place test objects below `build/test/out/<test name>/`.
|
|
159
|
+
- `form_release_build_objects_filelist` and `form_release_dependencies_filelist` place release objects and their `.d` files.
|
|
160
|
+
- `form_release_dependencies_filepath` mirrors a release object's own subpath below the release dependencies directory.
|
|
161
|
+
- `preprocessed_file_subpath` places preprocessed headers and sources below each test's preprocessing directories.
|
|
162
|
+
- The gcov and Bullseye plugins call `form_test_object_filepath` with their own context. Their untested sources land below `build/<plugin>/out/` by the same rule.
|
|
163
|
+
|
|
164
|
+
### Test names
|
|
165
|
+
|
|
166
|
+
A test's name is its path from the project root without its extension. `form_test_name(filepath)` is `BuildSubpath.encode( filepath.ext( '' ) )`. The name places the test's build output, results, runner, mocks, preprocessing, and logs. `test/unit/test_foo.c` is named `test/unit/test_foo`. A test at `../tests/test_x.c` is named `_up_/tests/test_x`. `TestBuildValidations` stops the build through `DestinationCollision` when two tests share a name.
|
|
167
|
+
|
|
168
|
+
### The Dependencies plugin
|
|
169
|
+
|
|
170
|
+
The Dependencies plugin places objects by a different root. `Dependencies#prepared_object` takes the source's subdirectory below the dependency's own `:source` path through `ProjectPaths.subdir_below`. A dependency builds from its own tree, so its project root is that tree.
|
|
171
|
+
|
|
172
|
+
## Mocks: `MockLocator`
|
|
173
|
+
|
|
174
|
+
Mocks keep placement relative to their include root, not the project root. CMock's folder semantics need it. The test's search path `-I build/test/mocks/<test>` must resolve `#include "drv/mock_gpio.h"` as written. A mock's identity, its CMock symbols and runner entries, also comes from that path.
|
|
175
|
+
|
|
176
|
+
`FileFinder#resolve_mock` finds the mocked header. It then takes the header's subdirectory below the most specific root of `collection_mirror_roots_headers` through `PathMirror.relative_subdir_from_clean_roots`. Containment is by whole directories, so a header outside every root yields `''`. Its mock then lands flat in the test's mock directory.
|
|
177
|
+
|
|
178
|
+
`MockLocator#placement(mocks_root:, subdir:, include_path:)` returns a `MockPlacement` struct with four fields.
|
|
179
|
+
|
|
180
|
+
- `folder` is the `#include` text's directory when `:cmock` ↳ `:mock_name_include_folder` is on, otherwise nil.
|
|
181
|
+
- `directory` is where the mock's files are generated.
|
|
182
|
+
- `root` is the mock path CMock is configured with. CMock writes the mock at `folder` below it, which leads to `directory`.
|
|
183
|
+
- `reach` is the search path from which the test's `#include` text reaches `directory`.
|
|
184
|
+
|
|
185
|
+
An `#include` folder that climbs with `..` needs one more step. `written_spot` places the mock below `mocks_root/_up_/…` and sets `reach` to an anchor of `_up_` segments. The `#include` then climbs from that anchor back down to the mock, and the mock never leaves the test's mock directory. `MockLocator#search_directories` gives the test `[directory, reach].uniq` as search paths. `StandinHeaderinator` creates `reach` when it lies outside `directory`.
|
|
186
|
+
|
|
187
|
+
## Finding inputs from build files: `FileFinder`
|
|
188
|
+
|
|
189
|
+
A lookup from a build file inverts placement exactly. It finds the input by path before it matches by name, so a same-named file elsewhere never interferes.
|
|
190
|
+
|
|
191
|
+
`mirrored_query(filepath, release:, test:, context:)` turns an object path into a source query.
|
|
192
|
+
|
|
193
|
+
1. A path outside the build root is a source-tree query from an `#include` or `TEST_SOURCE_FILE()`. `header_root_relative_query` handles it.
|
|
194
|
+
2. Inside the build root, `placement_root` names the release output root or the test's build path.
|
|
195
|
+
3. The subdirectory below that root is decoded by `BuildSubpath.decode` and joined with the basename. The query is marked exact.
|
|
196
|
+
|
|
197
|
+
`find_compiled_input` tries an exact query first through `exact_file`. `exact_file` builds an index per collection, keyed by `compare_by_identity` and then by absolute path. A tail match by trailing segments follows only when the exact query misses. It also serves a query that is not exact, such as an object below a third-party plugin's build root that placement does not know.
|
|
198
|
+
|
|
199
|
+
`test_context_of(filepath)` recovers a test name from a path below `build/test/out/`. Objects nest below the name at their own paths, so it returns the longest known test name that the directory begins with.
|
|
200
|
+
|
|
201
|
+
`header_root_relative_query(filepath)` correlates a header with the source beside it. Headers and sources often sit below different roots, such as `include/` and `src/`. The query is the header's subdirectory below its root in `collection_mirror_roots_include`, so `include/drv/gpio.h` looks for a source whose path ends in `drv/gpio`. `Partializer` uses the same roots for module sources.
|
|
202
|
+
|
|
203
|
+
## Collecting paths: `FilePathCollectionUtils`
|
|
204
|
+
|
|
205
|
+
Collection turns `:paths` entries into directories and `:files` revisions into files. Each place is collected once and spelled through its entry's real base. Exclusions are judged by distance between places, never by text.
|
|
206
|
+
|
|
207
|
+
### Spellings and distance
|
|
208
|
+
|
|
209
|
+
`PathSpellings` holds the tools collection compares with. `PathSpellings.of(paths)` is the Set of both spellings of each path, absolute and real, as compared. `PathSpellings.key` folds case on Windows only. `PathSpellings.through_real(paths, base)` respells paths found below `base` through `base`'s real path, so a project reached through a symlink never collects one directory twice. `PathSpellings.nearer(spellings, spelled)` walks up from a place and counts the steps to the nearest place in `spelled`. It returns 0 for the place itself and infinity for none.
|
|
210
|
+
|
|
211
|
+
### Directories from `:paths`
|
|
212
|
+
|
|
213
|
+
`collect_paths(paths, exclude:, reserved:)` collects directories in three steps.
|
|
214
|
+
|
|
215
|
+
1. `named_directories` lists every directory each additive entry names. `entry_directories` globs the entry, adds Ceedling's parent directories for a `/**` or `/*` tail, and respells the results through the entry's real base. A bracket in a path is literal, never glob syntax.
|
|
216
|
+
2. Each directory becomes a `Named` struct of `spelled`, `spellings`, and `distance`. `distance` is how far below the nearest base of the entries naming it the directory lies. A directory reached by two entries keeps its first spelling and its smallest distance.
|
|
217
|
+
3. A `PathExclusions` decides which directories stay. The kept ones return relative to the working directory.
|
|
218
|
+
|
|
219
|
+
### The three exclusions
|
|
220
|
+
|
|
221
|
+
`PathExclusions` is a Struct of three spelling sets, built by `PathExclusions.of(minus, excluded, reserved)`. `keeps?(spellings, distance)` keeps a place only if all three tests pass.
|
|
222
|
+
|
|
223
|
+
| Set | Source | A place stays when |
|
|
224
|
+
| --- | --- | --- |
|
|
225
|
+
| `minus` | `-:` entries | It lies strictly nearer its entry's base than any `-:` place above it |
|
|
226
|
+
| `excluded` | The build root | It lies no farther from its entry's base than the build root above it |
|
|
227
|
+
| `reserved` | `BuildRootPolicy.reserved` | No reserved directory lies at or above it |
|
|
228
|
+
|
|
229
|
+
The `minus` rule lets `+:src/legacy/keep` survive beside `-:src/legacy`, because its own entry names it more closely. The `excluded` rule lets `../**` or `./**` sweep the project without reaching into `build/`. An entry based at or below the build root, such as `build/gen/**`, still collects there. The `reserved` rule has no override, so no entry collects Ceedling's own output.
|
|
230
|
+
|
|
231
|
+
### Files from `:files`
|
|
232
|
+
|
|
233
|
+
`revise_filelist(list, revisions, exclude:, reserved:)` adds and removes files. `revision_files` globs each revision and respells it through its real base. `added_files` keeps an additive revision's file only if `PathExclusions#keeps?` passes. The distance is measured from the revision's own base, so `+:**/*.c` adds nothing from `build/`, and `+:build/gen/*.c` adds what it names. `kept_files` then drops duplicates and subtracted files by `real_key`, which compares a file through its directory's real path. The result is a literal FileList, so no entry is read as a glob again.
|
|
234
|
+
|
|
235
|
+
### Mirror roots
|
|
236
|
+
|
|
237
|
+
`collect_mirror_roots(entries)` computes the roots that mocks and header lookups measure against, once, at configuration time. It strips a `/**` or `/*` tail, expands a glob in the middle of an entry into one root per directory, and canonicalizes each. `ConfiguratorBuilder#collect_mirror_roots` stores two lists. `collection_mirror_roots_include` comes from `:paths` ↳ `:include`. `collection_mirror_roots_headers` adds `:test` and `:support`.
|
|
238
|
+
|
|
239
|
+
## The build root: `BuildRootPolicy`
|
|
240
|
+
|
|
241
|
+
`BuildRootPolicy` says what a build root may be and what in it collection must never return. `clobber` empties the directories Ceedling creates there, so the policy protects the project from them.
|
|
242
|
+
|
|
243
|
+
- `problem(root)` refuses an empty build root, the project directory, or any directory above it. It compares canonical spellings, so a symlinked spelling of the project is refused too. `ConfiguratorPathsValidator#validate_build_root` logs `message(root, problem)` as an error.
|
|
244
|
+
- `entries_within(root, config)` lists each additive `:paths` and `:files` entry whose base lies in the build root. `warnings` turns each into a COMPLAIN message. Such entries collect, but the warning names them.
|
|
245
|
+
- `reserved(config)` lists the directories Ceedling creates. It reads the settings in `CREATED`: the artifacts, test, release, vendor, and cache directories, the log path, and the mock path. It adds `<build_root>/<plugin>` for each enabled plugin. An empty setting names nothing, so the project directory is never reserved by accident.
|
|
246
|
+
|
|
247
|
+
`ConfiguratorBuilder#build_exclusions` passes the build root as `exclude:` and `reserved` as `reserved:` to every `:paths` collection and every `:files` revision. `ConfiguratorBuilder#without_tests` then removes test files from the source collection after revisions, with a warning. It removes them from the release build input without a second warning.
|
|
248
|
+
|
|
249
|
+
A `-:` entry that matches nothing is never an error. `ConfiguratorPathsValidator#subtracts_nothing` warns of one outside the build root and stays silent below it. Below the build root nothing exists before a first build.
|
|
250
|
+
|
|
251
|
+
## Other intake points
|
|
252
|
+
|
|
253
|
+
Several settings reach paths outside `:paths` and `:files`. Each spells its paths through `ProjectPaths` at intake.
|
|
254
|
+
|
|
255
|
+
- `TestContextExtractor` canonicalizes every `TEST_INCLUDE_PATH()` entry. `IncludePathinator#prioritized_header_files` then ranks headers by walking up each header's directories to the nearest search root, with `rank_key` folding case on Windows.
|
|
256
|
+
- `LibraryResolver#search_directories` collects `:paths` ↳ `:libraries` through `collect_paths` and memoizes the result. `search_path_arguments` forms the `-L` arguments from it.
|
|
257
|
+
- `ConfiguratorBuilder#set_release_target` standardizes `:release_build` ↳ `:output`. An absolute output names the artifact itself.
|
|
258
|
+
- `CliHelper#process_logging_path` applies inline Ruby expansion and `standardize` to the build root before configuration loads. It refuses one that expands to nothing.
|
|
259
|
+
- `RakefileComponentResolver.prepare_plugin_load_paths` standardizes `:plugins` ↳ `:load_paths` with the same expander `lib/` uses.
|
|
260
|
+
- `DependencySpec` in the Dependencies plugin standardizes each dependency's paths.
|
|
261
|
+
- `Setupinator` runs `Configurator#populate_cmock_helpers` after `standardize_paths`, so the Unity helper paths are already standardized.
|
|
262
|
+
|
|
263
|
+
## Platform differences
|
|
264
|
+
|
|
265
|
+
Paths behave differently on Windows, macOS, and other POSIX systems. The code handles each difference in one place. `ProjectPaths::WINDOWS` selects the Windows behavior from `RbConfig::CONFIG['host_os']`.
|
|
266
|
+
|
|
267
|
+
### Windows
|
|
268
|
+
|
|
269
|
+
- **Case.** Comparison folds case through `same?`, `starts_with?`, `PathSpellings.key`, and `IncludePathinator#rank_key`. Stored spellings keep their case.
|
|
270
|
+
- **Drive letters.** `absolute` upper-cases the drive letter. `ROOTED` requires one for a clean absolute path.
|
|
271
|
+
- **`_abs_`.** Only Windows produces `_abs_`. A path on another drive has no relative form, and `Pathname#relative_path_from` raises `ArgumentError`. `relative` then returns the absolute path, and `encode` places it under `_abs_/<drive>/…`.
|
|
272
|
+
- **UNC shares.** `unc?` is true only on Windows. `unc_absolute` resolves `..` but never climbs above `//server/share`. A share encodes under `_unc_`.
|
|
273
|
+
- **Unclean segments.** `File.expand_path` lengthens a short `~1` name and drops a trailing dot or space. `UNCLEAN` therefore treats `~`, a trailing dot, and a trailing space as unclean, so those paths always pass through `File.expand_path`.
|
|
274
|
+
- **Path length.** Placement by project path lengthens build paths. A source far from the project gains one `_up_` per climbing level. `build_filepath` checks each path once against the 260-character limit through `FileWrapper#check_path_length`, which warns. `_ceedling_` keeps installed files short.
|
|
275
|
+
- **Real-path casing.** Intake takes a path's on-disk casing from `File.realpath`, along the existing part of the path.
|
|
276
|
+
|
|
277
|
+
### macOS
|
|
278
|
+
|
|
279
|
+
- **Symlinked temporary directories.** `/tmp` and `/var` are symlinks into `/private`. `working_dir` and `canonical` resolve them, so `/tmp/proj` and `/private/tmp/proj` compare as one place.
|
|
280
|
+
- **Case-insensitive volumes.** APFS and HFS+ usually ignore case, but Ceedling compares case exactly outside Windows. Two spellings that differ only by case count as two places. `MARKER_NAMED` ignores case so that `_UP_` is still escaped.
|
|
281
|
+
|
|
282
|
+
### Linux and other POSIX systems
|
|
283
|
+
|
|
284
|
+
- **Absolute paths.** Every absolute path has a relative form. A header at `/opt/sdk/inc` encodes as a chain of `_up_` segments followed by `opt/sdk/inc`, never as `_abs_`. The chain grows with the project's depth below `/`.
|
|
285
|
+
- **Doubled separators.** A leading `//` is the filesystem root, not a share. `absolute` squeezes it to `/`.
|
|
286
|
+
|
|
287
|
+
## Changing path code
|
|
288
|
+
|
|
289
|
+
A change to path code keeps the invariants by following the patterns above. These checks catch the usual mistakes.
|
|
290
|
+
|
|
291
|
+
- Compare places with `ProjectPaths.contains?` or `subdir_below`. Never compare path text with `start_with?` or `include?`.
|
|
292
|
+
- Spell configured paths with `standardize` at intake and `canonical` where a symlinked spelling matters.
|
|
293
|
+
- Place build files through `form_build_subpath` or `build_filepath`. Never join a source's directory under a build root directly.
|
|
294
|
+
- Run a new list of outputs through `DestinationCollision`.
|
|
295
|
+
- Add a marker by updating `MARKER_NAMED`, `encode`, and `decode` together, with a round-trip spec.
|
|
296
|
+
- Keep `ProjectPaths`, `BuildSubpath`, `PathSpellings`, and `PathExclusions` free of DIY objects and configuration.
|
|
297
|
+
|
|
298
|
+
## Specs
|
|
299
|
+
|
|
300
|
+
Each tier pins a different layer of this design.
|
|
301
|
+
|
|
302
|
+
- **Unit.** `spec/units/filesystem/project_paths_spec.rb`, `build_subpath_spec.rb`, `path_mirror_spec.rb`, `path_matcher_spec.rb`, `destination_collision_spec.rb`, `file_path_utils_spec.rb`, `file_path_collection_utils_spec.rb`, and `file_finder_spec.rb`, with `spec/units/mock_locator_spec.rb`, `spec/units/library_resolver_spec.rb`, and `spec/units/config/build_root_policy_spec.rb`. Windows drive and UNC examples run only on Windows.
|
|
303
|
+
- **Integration.** `spec/integration/path_scenarios_spec.rb` and `path_intake_scenarios_spec.rb` configure real projects in temporary workspaces through `spec/support/integration/path_scenario_harness.rb`. `expect_collections_outside_build` and `expect_one_spelling_each` assert the invariants.
|
|
304
|
+
- **System.** `spec/system/path_handling_probes_spec.rb`, `build_output_mirroring_spec.rb`, `rake_release_task_path_disambiguation_spec.rb`, `bracket_path_handling_spec.rb`, and `special_path_characters_spec.rb` run full builds.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Files and Paths
|
|
2
|
+
|
|
3
|
+
This directory holds the code that spells, compares, collects, places, and finds paths, and the code that reads, writes, and copies files. Everything else in Ceedling reaches the filesystem through it.
|
|
4
|
+
|
|
5
|
+
The pure modules depend on no DIY object or configuration, so the launcher in `bin/` uses them too. The DIY objects are built from `lib/ceedling/objects.yml`, and the launcher builds `FileWrapper` from `bin/objects.yml`. Code requires any file here as `ceedling/filesystem/<name>`. Unit specs mirror this directory in `spec/units/filesystem/`.
|
|
6
|
+
|
|
7
|
+
Two design documents sit beside the code. [PATH_HANDLING.md](PATH_HANDLING.md) covers path spelling, containment, and build file placement. [FILE_TREE.md](FILE_TREE.md) covers walking and copying file trees without `lstat`.
|
|
8
|
+
|
|
9
|
+
## Path spelling and placement
|
|
10
|
+
|
|
11
|
+
Pure modules with no state beyond memos.
|
|
12
|
+
|
|
13
|
+
| File | Module | Role |
|
|
14
|
+
| --- | --- | --- |
|
|
15
|
+
| [`project_paths.rb`](project_paths.rb) | `ProjectPaths` | One spelling for every path. Absolute, relative, and canonical forms, and containment by whole directories |
|
|
16
|
+
| [`working_dir.rb`](working_dir.rb) | `WorkingDir` | The working directory, read once, and `chdir`, the one way Ceedling code changes it |
|
|
17
|
+
| [`build_subpath.rb`](build_subpath.rb) | `BuildSubpath` | A build file's place from its source's path from the project root, with the `_up_`, `_abs_`, `_unc_`, and `_ceedling_` markers, and back |
|
|
18
|
+
| [`path_spellings.rb`](path_spellings.rb) | `PathSpellings` | A place's listed and real spellings, and how far one place lies below another |
|
|
19
|
+
| [`path_exclusions.rb`](path_exclusions.rb) | `PathExclusions` | What collection leaves out: `-:` entries, excluded places, and reserved places |
|
|
20
|
+
| [`path_mirror.rb`](path_mirror.rb) | `PathMirror` | A file's subdirectory below the most specific configured root that contains it |
|
|
21
|
+
| [`path_matcher.rb`](path_matcher.rb) | `PathMatcher` | Resolving `..` in a query, and matching a query's trailing segments against a collection |
|
|
22
|
+
| [`destination_collision.rb`](destination_collision.rb) | `DestinationCollision` | Stopping a build when two inputs map to one output |
|
|
23
|
+
|
|
24
|
+
## Path services
|
|
25
|
+
|
|
26
|
+
DIY objects that apply the modules above to a project's configuration.
|
|
27
|
+
|
|
28
|
+
| File | Class | Role |
|
|
29
|
+
| --- | --- | --- |
|
|
30
|
+
| [`file_path_utils.rb`](file_path_utils.rb) | `FilePathUtils` | Test names and the path of every build file |
|
|
31
|
+
| [`file_path_collection_utils.rb`](file_path_collection_utils.rb) | `FilePathCollectionUtils` | Collecting `:paths` directories and `:files` revisions, with exclusions applied |
|
|
32
|
+
| [`file_finder.rb`](file_finder.rb) | `FileFinder` | Finding a build file's input from the build file's own path |
|
|
33
|
+
| [`file_finder_helper.rb`](file_finder_helper.rb) | `FileFinderHelper` | Resolving a query against a collection: its one match, the first of several with a notice, or a report of none |
|
|
34
|
+
|
|
35
|
+
## File I/O
|
|
36
|
+
|
|
37
|
+
| File | Module or class | Role |
|
|
38
|
+
| --- | --- | --- |
|
|
39
|
+
| [`file_wrapper.rb`](file_wrapper.rb) | `FileWrapper` | The DIY object most code uses to read, write, list, and copy files. It checks path lengths, retries transient I/O errors, and replaces files whole; this class is mocked for unit testing against |
|
|
40
|
+
| [`file_tree.rb`](file_tree.rb) | `FileTree` | A pure module that walks a tree classifying entries by `stat`, and brings a copy up to date, replacing entries of the wrong type. See [FILE_TREE.md](FILE_TREE.md) |
|
|
41
|
+
| [`file_system_wrapper.rb`](file_system_wrapper.rb) | `FileSystemWrapper` | Changing the working directory for the length of a block, through `WorkingDir` |
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
# =========================================================================
|
|
2
|
+
# Ceedling - Test-Centered Build System for C
|
|
3
|
+
# ThrowTheSwitch.org
|
|
4
|
+
# Copyright (c) 2010-26 Mike Karlesky, Mark VanderVoord, & Greg Williams
|
|
5
|
+
# SPDX-License-Identifier: MIT
|
|
6
|
+
# =========================================================================
|
|
7
|
+
|
|
8
|
+
require 'ceedling/filesystem/project_paths'
|
|
9
|
+
|
|
10
|
+
# Where a build places a file of a path: by its path from the project root, so two files
|
|
11
|
+
# never share a build file and none lands outside the build directory. `..` becomes `_up_`,
|
|
12
|
+
# a path on another drive goes under `_abs_` or `_unc_`, and a file the running Ceedling
|
|
13
|
+
# installation ships, outside the project, goes under `_ceedling_`. A directory of a path
|
|
14
|
+
# that is itself named like one of these gains a leading `_`, so `decode` inverts `encode`
|
|
15
|
+
# exactly.
|
|
16
|
+
module BuildSubpath
|
|
17
|
+
|
|
18
|
+
PARENT_SEGMENT = '_up_'
|
|
19
|
+
ABSOLUTE_SEGMENT = '_abs_'
|
|
20
|
+
UNC_SEGMENT = '_unc_'
|
|
21
|
+
INSTALLED_SEGMENT = '_ceedling_'
|
|
22
|
+
|
|
23
|
+
# A segment of a path named like a marker, perhaps already led by more `_`, in any case,
|
|
24
|
+
# since macOS and Windows file systems fold it
|
|
25
|
+
MARKER_NAMED = /\A_+(?:up|abs|unc|ceedling)_\z/i
|
|
26
|
+
|
|
27
|
+
# The running Ceedling installation, above lib/ceedling/filesystem/
|
|
28
|
+
INSTALLATION_ROOT = File.expand_path( '../../..', __dir__ )
|
|
29
|
+
|
|
30
|
+
# The directories of an installation holding what Ceedling ships. A project may itself sit
|
|
31
|
+
# elsewhere in an installation, as in a development checkout, and its files are its own.
|
|
32
|
+
INSTALLED_DIRS = %w[assets bin lib plugins vendor].map { |dir| File.join( INSTALLATION_ROOT, dir ) }.freeze
|
|
33
|
+
|
|
34
|
+
@lock = Mutex.new
|
|
35
|
+
@subpaths = {}
|
|
36
|
+
|
|
37
|
+
# The build subpath that places a file of `path` by its path from the project root
|
|
38
|
+
def self.encode(path)
|
|
39
|
+
full = ProjectPaths.absolute( path )
|
|
40
|
+
rel = ProjectPaths.relative( full )
|
|
41
|
+
|
|
42
|
+
# Another drive, with no relative path to the project, lies outside it as a climb does
|
|
43
|
+
beyond = !ProjectPaths.contains?( ProjectPaths.working_dir, full )
|
|
44
|
+
return installed_segments( full ).join( '/' ) if beyond && installed?( full )
|
|
45
|
+
return absolute_segments( rel ).join( '/' ) if ProjectPaths.absolute?( rel )
|
|
46
|
+
|
|
47
|
+
return ProjectPaths.segments( rel ).map { |segment| segment == '..' ? PARENT_SEGMENT : escape( segment ) }.join( '/' )
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# The path, in intake's spelling, that `encode` made `subpath` from
|
|
51
|
+
def self.decode(subpath)
|
|
52
|
+
segments = ProjectPaths.segments( subpath )
|
|
53
|
+
rest = unescaped( segments.drop( 1 ) )
|
|
54
|
+
|
|
55
|
+
case segments.first
|
|
56
|
+
when INSTALLED_SEGMENT then return ProjectPaths.relative( File.join( INSTALLATION_ROOT, *rest ) )
|
|
57
|
+
when UNC_SEGMENT then return "//#{rest.join( '/' )}"
|
|
58
|
+
when ABSOLUTE_SEGMENT then return "#{segments[1]}:/#{rest.drop( 1 ).join( '/' )}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
return segments.map { |segment| segment == PARENT_SEGMENT ? '..' : unescape( segment ) }.join( '/' )
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# `encode` for a directory, memoized, since a build places many files from each directory.
|
|
65
|
+
# The working directory itself encodes as ''.
|
|
66
|
+
def self.of_directory(dir)
|
|
67
|
+
key = [ProjectPaths.working_dir, dir.to_s]
|
|
68
|
+
return @lock.synchronize { @subpaths[key] ||= encode( dir ) }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def self.installed?(full)
|
|
72
|
+
return INSTALLED_DIRS.any? { |dir| ProjectPaths.contains?( dir, full ) }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def self.installed_segments(full)
|
|
76
|
+
return [INSTALLED_SEGMENT] + ProjectPaths.segments( full[INSTALLATION_ROOT.length..] ).map { |segment| escape( segment ) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.escape(segment)
|
|
80
|
+
return segment.match?( MARKER_NAMED ) ? "_#{segment}" : segment
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def self.unescape(segment)
|
|
84
|
+
return segment.match?( MARKER_NAMED ) && segment.start_with?( '__' ) ? segment[1..] : segment
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def self.unescaped(segments)
|
|
88
|
+
return segments.map { |segment| unescape( segment ) }
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# An absolute path's segments below `_abs_`, a drive letter as a plain segment, or below
|
|
92
|
+
# `_unc_` for a share
|
|
93
|
+
def self.absolute_segments(path)
|
|
94
|
+
return [UNC_SEGMENT] + ProjectPaths.segments( path ).map { |segment| escape( segment ) } if ProjectPaths.unc?( path )
|
|
95
|
+
|
|
96
|
+
parts = ProjectPaths.segments( path )
|
|
97
|
+
return [ABSOLUTE_SEGMENT] + parts.each_with_index.map { |part, index| index.zero? ? part.delete_suffix( ':' ) : escape( part ) }
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
private_class_method :installed?, :installed_segments, :absolute_segments, :escape, :unescape, :unescaped
|
|
101
|
+
|
|
102
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# =========================================================================
|
|
2
|
+
# Ceedling - Test-Centered Build System for C
|
|
3
|
+
# ThrowTheSwitch.org
|
|
4
|
+
# Copyright (c) 2010-26 Mike Karlesky, Mark VanderVoord, & Greg Williams
|
|
5
|
+
# SPDX-License-Identifier: MIT
|
|
6
|
+
# =========================================================================
|
|
7
|
+
|
|
8
|
+
require 'ceedling/exceptions'
|
|
9
|
+
|
|
10
|
+
# Two inputs a build maps to one output, such as two tests to one build name or two sources
|
|
11
|
+
# to one object, would leave one of them out of the build, so a build stops on either
|
|
12
|
+
module DestinationCollision
|
|
13
|
+
|
|
14
|
+
# Each output more than one of `inputs` maps to, with the inputs mapping to it
|
|
15
|
+
def self.find(inputs, &output)
|
|
16
|
+
return inputs.uniq.group_by( &output ).select { |_output, sharing| sharing.size > 1 }
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Stops the build, naming `inputs` and the `output` they share by `relation`
|
|
20
|
+
def self.raise_for(output, inputs, relation)
|
|
21
|
+
names = inputs.map { |input| "`#{input}`" }.join( ' and ' )
|
|
22
|
+
raise CeedlingException.new( "#{names} #{relation} `#{output}`. Rename one, or move it to its own directory." )
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
end
|